From 018ca18a4e3af73e48c1e910bfabeab1fbe40e78 Mon Sep 17 00:00:00 2001 From: Awais Ansari Date: Tue, 1 Aug 2023 18:50:12 +0500 Subject: [PATCH] feat: deprecate notification tray in learning header --- example/index.js | 3 +- package-lock.json | 68 +------- package.json | 8 +- src/Header.test.jsx | 26 +-- src/Notifications/NotificationRowItem.jsx | 79 --------- src/Notifications/NotificationSections.jsx | 96 ----------- src/Notifications/NotificationTabs.jsx | 53 ------ src/Notifications/data/__factories__/index.js | 1 - .../__factories__/notifications.factory.js | 31 ---- src/Notifications/data/api.js | 39 ----- src/Notifications/data/api.test.js | 147 ----------------- src/Notifications/data/hook.js | 11 -- src/Notifications/data/index.js | 1 - src/Notifications/data/notifications.json | 134 --------------- src/Notifications/data/redux.test.js | 155 ------------------ src/Notifications/data/selector.test.jsx | 115 ------------- src/Notifications/data/selectors.js | 23 --- src/Notifications/data/slice.js | 147 ----------------- src/Notifications/data/thunks.js | 135 --------------- src/Notifications/index.jsx | 103 ------------ src/Notifications/index.test.jsx | 109 ------------ src/Notifications/messages.js | 36 ---- .../notificationRowItem.test.jsx | 87 ---------- .../notificationSections.test.jsx | 106 ------------ src/Notifications/notificationTabs.test.jsx | 91 ---------- src/Notifications/test-utils.js | 32 ---- src/Notifications/utils.js | 52 ------ src/common/time-locale.js | 18 -- src/index.scss | 137 +--------------- .../AuthenticatedUserDropdown.jsx | 19 +-- src/learning-header/LearningHeader.jsx | 39 ++--- src/store.js | 16 -- 32 files changed, 36 insertions(+), 2081 deletions(-) delete mode 100644 src/Notifications/NotificationRowItem.jsx delete mode 100644 src/Notifications/NotificationSections.jsx delete mode 100644 src/Notifications/NotificationTabs.jsx delete mode 100644 src/Notifications/data/__factories__/index.js delete mode 100644 src/Notifications/data/__factories__/notifications.factory.js delete mode 100644 src/Notifications/data/api.js delete mode 100644 src/Notifications/data/api.test.js delete mode 100644 src/Notifications/data/hook.js delete mode 100644 src/Notifications/data/index.js delete mode 100644 src/Notifications/data/notifications.json delete mode 100644 src/Notifications/data/redux.test.js delete mode 100644 src/Notifications/data/selector.test.jsx delete mode 100644 src/Notifications/data/selectors.js delete mode 100644 src/Notifications/data/slice.js delete mode 100644 src/Notifications/data/thunks.js delete mode 100644 src/Notifications/index.jsx delete mode 100644 src/Notifications/index.test.jsx delete mode 100644 src/Notifications/messages.js delete mode 100644 src/Notifications/notificationRowItem.test.jsx delete mode 100644 src/Notifications/notificationSections.test.jsx delete mode 100644 src/Notifications/notificationTabs.test.jsx delete mode 100644 src/Notifications/test-utils.js delete mode 100644 src/Notifications/utils.js delete mode 100644 src/common/time-locale.js delete mode 100644 src/store.js diff --git a/example/index.js b/example/index.js index deba37a..e9b44a5 100644 --- a/example/index.js +++ b/example/index.js @@ -4,8 +4,7 @@ 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 { LearningHeader as Header } from '@edx/frontend-component-header'; +import Header from '@edx/frontend-component-header'; import './index.scss'; diff --git a/package-lock.json b/package-lock.json index 4c4ba4b..4f25872 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,17 +15,11 @@ "@fortawesome/free-regular-svg-icons": "6.4.0", "@fortawesome/free-solid-svg-icons": "6.4.0", "@fortawesome/react-fontawesome": "^0.2.0", - "@reduxjs/toolkit": "1.9.5", "axios-mock-adapter": "1.21.5", "babel-polyfill": "6.26.0", - "classnames": "2.3.2", - "lodash": "4.17.21", - "react-redux": "7.2.9", "react-responsive": "8.2.0", "react-router-dom": "5.3.4", - "react-transition-group": "4.4.5", - "rosie": "2.1.0", - "timeago.js": "4.0.2" + "react-transition-group": "4.4.5" }, "devDependencies": { "@edx/brand": "npm:@edx/brand-openedx@1.2.0", @@ -5559,29 +5553,6 @@ "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==", "dev": true }, - "node_modules/@reduxjs/toolkit": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", - "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", - "dependencies": { - "immer": "^9.0.21", - "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "reselect": "^4.1.8" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.0.2" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, "node_modules/@restart/context": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", @@ -6570,7 +6541,7 @@ "version": "7.1.25", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==", - "devOptional": true, + "dev": true, "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -12782,6 +12753,7 @@ "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -16540,7 +16512,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -19422,7 +19395,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "devOptional": true + "dev": true }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", @@ -19479,7 +19452,7 @@ "version": "7.2.9", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", - "devOptional": true, + "dev": true, "dependencies": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -19874,6 +19847,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dev": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -19887,14 +19861,6 @@ "@redux-saga/core": "^1.2.3" } }, - "node_modules/redux-thunk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", - "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", - "peerDependencies": { - "redux": "^4" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -20164,11 +20130,6 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, - "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" - }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -20294,14 +20255,6 @@ "rimraf": "bin.js" } }, - "node_modules/rosie": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/rosie/-/rosie-2.1.0.tgz", - "integrity": "sha512-Dbzdc+prLXZuB/suRptDnBUY29SdGvND3bLg6cll8n7PNqzuyCxSlRfrkn8PqjS9n4QVsiM7RCvxCkKAkTQRjA==", - "engines": { - "node": ">=10" - } - }, "node_modules/rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", @@ -22405,11 +22358,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "node_modules/timeago.js": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", - "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" - }, "node_modules/tiny-invariant": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", diff --git a/package.json b/package.json index f2cba3c..0275a5a 100644 --- a/package.json +++ b/package.json @@ -62,17 +62,11 @@ "@fortawesome/free-regular-svg-icons": "6.4.0", "@fortawesome/free-solid-svg-icons": "6.4.0", "@fortawesome/react-fontawesome": "^0.2.0", - "@reduxjs/toolkit": "1.9.5", "axios-mock-adapter": "1.21.5", "babel-polyfill": "6.26.0", - "classnames": "2.3.2", - "lodash": "4.17.21", - "react-redux": "7.2.9", "react-responsive": "8.2.0", "react-router-dom": "5.3.4", - "react-transition-group": "4.4.5", - "rosie": "2.1.0", - "timeago.js": "4.0.2" + "react-transition-group": "4.4.5" }, "peerDependencies": { "@edx/frontend-platform": "^4.0.0", diff --git a/src/Header.test.jsx b/src/Header.test.jsx index 7636460..51fef20 100644 --- a/src/Header.test.jsx +++ b/src/Header.test.jsx @@ -2,38 +2,24 @@ import React from 'react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import TestRenderer from 'react-test-renderer'; -import { AppContext, AppProvider } from '@edx/frontend-platform/react'; +import { AppContext } 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 }) => ( - - -
- - + +
+ ); describe('
', () => { - beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: '123abc', - username: 'testuser', - administrator: false, - roles: [], - }, - }); - }); it('renders correctly for anonymous desktop', () => { const contextValue = { authenticatedUser: null, diff --git a/src/Notifications/NotificationRowItem.jsx b/src/Notifications/NotificationRowItem.jsx deleted file mode 100644 index 049c949..0000000 --- a/src/Notifications/NotificationRowItem.jsx +++ /dev/null @@ -1,79 +0,0 @@ -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 * 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 ( - - -
-
-
- -
- - {courseName} - - {intl.formatMessage(messages.fullStop)} - {timeago.format(createdAt, 'time-locale')} - - -
-
- {!lastRead && ( -
- -
- )} -
-
-
- ); -}; - -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); diff --git a/src/Notifications/NotificationSections.jsx b/src/Notifications/NotificationSections.jsx deleted file mode 100644 index 864fcbf..0000000 --- a/src/Notifications/NotificationSections.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import { Button, Spinner } 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, selectNotificationStatus, -} from './data/selectors'; -import { splitNotificationsByTime } from './utils'; -import { updatePaginationRequest, RequestStatus } from './data/slice'; - -const NotificationSections = () => { - const intl = useIntl(); - const dispatch = useDispatch(); - const selectedAppName = useSelector(selectSelectedAppName()); - const notificationRequestStatus = useSelector(selectNotificationStatus()); - const notifications = useSelector(selectNotificationsByIds(selectedAppName)); - const { hasMorePages } = 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 ( -
-
- - {section === 'today' && intl.formatMessage(messages.notificationTodayHeading)} - {section === 'earlier' && intl.formatMessage(messages.notificationEarlierHeading)} - - {notifications?.length > 0 && (section === 'earlier' ? today.length === 0 : true) && ( - - )} -
- {items.map((notification) => ( - - ))} -
- ); - }; - - return ( -
- {renderNotificationSection('today', today)} - {renderNotificationSection('earlier', earlier)} - {hasMorePages && notificationRequestStatus === RequestStatus.IN_PROGRESS ? ( -
- -
- ) : (hasMorePages && notificationRequestStatus === RequestStatus.SUCCESSFUL - && ( - - ) - )} -
- ); -}; - -export default React.memo(NotificationSections); diff --git a/src/Notifications/NotificationTabs.jsx b/src/Notifications/NotificationTabs.jsx deleted file mode 100644 index 83ecdea..0000000 --- a/src/Notifications/NotificationTabs.jsx +++ /dev/null @@ -1,53 +0,0 @@ -/* 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 })); - if (selectedAppName) { dispatch(markNotificationsAsSeen(selectedAppName)); } - }, [currentPage, selectedAppName]); - - const handleActiveTab = useCallback((appName) => { - dispatch(updateAppNameRequest({ appName })); - }, []); - - const tabArray = useMemo(() => notificationTabs?.map((appName) => ( - - {appName === selectedAppName && ()} - - )), [notificationUnseenCounts, selectedAppName, notificationTabs]); - - return ( - - {tabArray} - - ); -}; - -export default React.memo(NotificationTabs); diff --git a/src/Notifications/data/__factories__/index.js b/src/Notifications/data/__factories__/index.js deleted file mode 100644 index cdf7f0b..0000000 --- a/src/Notifications/data/__factories__/index.js +++ /dev/null @@ -1 +0,0 @@ -import './notifications.factory'; diff --git a/src/Notifications/data/__factories__/notifications.factory.js b/src/Notifications/data/__factories__/notifications.factory.js deleted file mode 100644 index 7abf45b..0000000 --- a/src/Notifications/data/__factories__/notifications.factory.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Factory } from 'rosie'; - -Factory.define('notificationsCount') - .attr('count', 45) - .attr('countByAppName', { - reminders: 10, - discussion: 20, - grades: 10, - authoring: 5, - }) - .attr('showNotificationsTray', true); - -Factory.define('notification') - .sequence('id') - .attr('type', 'post') - .sequence('content', ['id'], (idx, notificationId) => `

User ${idx} posts Hello and welcome to SC0x - ${notificationId}!

`) - .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); - -Factory.define('notificationsList') - .attr('next', null) - .attr('previous', null) - .attr('count', null, 2) - .attr('num_pages', null, 1) - .attr('current_page', null, 1) - .attr('start', null, 0) - .attr('results', ['results'], (results) => results || Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })); diff --git a/src/Notifications/data/api.js b/src/Notifications/data/api.js deleted file mode 100644 index 5d18f6b..0000000 --- a/src/Notifications/data/api.js +++ /dev/null @@ -1,39 +0,0 @@ -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 getNotificationsListApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`; -export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-seen/${appName}/`; -export const markNotificationAsReadApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/read/`; - -export async function getNotificationsList(appName, page) { - const params = snakeCaseObject({ appName, page }); - const { data } = await getAuthenticatedHttpClient().get(getNotificationsListApiUrl(), { params }); - return data; -} - -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().patch(markNotificationAsReadApiUrl(), params); - - return data; -} - -export async function markNotificationRead(notificationId) { - const params = snakeCaseObject({ notificationId }); - const { data } = await getAuthenticatedHttpClient().patch(markNotificationAsReadApiUrl(), params); - - return { data, id: notificationId }; -} diff --git a/src/Notifications/data/api.test.js b/src/Notifications/data/api.test.js deleted file mode 100644 index a905f6c..0000000 --- a/src/Notifications/data/api.test.js +++ /dev/null @@ -1,147 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { Factory } from 'rosie'; - -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { initializeMockApp } from '@edx/frontend-platform/testing'; - -import { - getNotificationsListApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, - getNotificationCounts, getNotificationsList, markNotificationSeen, markAllNotificationRead, markNotificationRead, -} from './api'; - -import './__factories__'; - -const notificationCountsApiUrl = getNotificationsCountApiUrl(); -const notificationsApiUrl = getNotificationsListApiUrl(); -const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussion'); -const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl(); - -let axiosMock = null; - -describe('Notifications API', () => { - beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: '123abc', - username: 'testuser', - administrator: false, - roles: [], - }, - }); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - Factory.resetAll(); - }); - - afterEach(() => { - axiosMock.reset(); - }); - - it('Successfully get notification counts for different tabs.', async () => { - axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); - - const { count, countByAppName } = await getNotificationCounts(); - - expect(count).toEqual(45); - expect(countByAppName.reminders).toEqual(10); - expect(countByAppName.discussion).toEqual(20); - expect(countByAppName.grades).toEqual(10); - expect(countByAppName.authoring).toEqual(5); - }); - - it.each([ - { statusCode: 404, message: 'Failed to get notification counts.' }, - { statusCode: 403, message: 'Denied to get notification counts.' }, - ])('%s for notification counts API.', async ({ statusCode, message }) => { - axiosMock.onGet(notificationCountsApiUrl).reply(statusCode, { message }); - try { - await getNotificationCounts(); - } catch (error) { - expect(error.response.status).toEqual(statusCode); - expect(error.response.data.message).toEqual(message); - } - }); - - it('Successfully get notifications.', async () => { - axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.build('notificationsList'))); - - const notifications = await getNotificationsList('discussion', 1); - - expect(notifications.results).toHaveLength(2); - }); - - it.each([ - { statusCode: 404, message: 'Failed to get notifications.' }, - { statusCode: 403, message: 'Denied to get notifications.' }, - ])('%s for notification API.', async ({ statusCode, message }) => { - axiosMock.onGet(notificationsApiUrl).reply(statusCode, { message }); - try { - await getNotificationsList('discussion', 1); - } catch (error) { - expect(error.response.status).toEqual(statusCode); - expect(error.response.data.message).toEqual(message); - } - }); - - it('Successfully marked all notifications as seen for selected app.', async () => { - axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200, { message: 'Notifications marked seen.' }); - - const { message } = await markNotificationSeen('discussion'); - - expect(message).toEqual('Notifications marked seen.'); - }); - - it.each([ - { statusCode: 404, message: 'Failed to mark all notifications as seen for selected app.' }, - { statusCode: 403, message: 'Denied to mark all notifications as seen for selected app.' }, - ])('%s for notification mark as seen API.', async ({ statusCode, message }) => { - axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode, { message }); - try { - await markNotificationSeen('discussion'); - } catch (error) { - expect(error.response.status).toEqual(statusCode); - expect(error.response.data.message).toEqual(message); - } - }); - - it('Successfully marked all notifications as read for selected app.', async () => { - axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' }); - - const { message } = await markAllNotificationRead('discussion'); - - expect(message).toEqual('Notifications marked read.'); - }); - - it.each([ - { statusCode: 404, message: 'Failed to mark all notifications as read for selected app.' }, - { statusCode: 403, message: 'Denied to mark all notifications as read for selected app.' }, - ])('%s for notification mark all as read API.', async ({ statusCode, message }) => { - axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message }); - try { - await markAllNotificationRead('discussion'); - } catch (error) { - expect(error.response.status).toEqual(statusCode); - expect(error.response.data.message).toEqual(message); - } - }); - - it('Successfully marked notification as read.', async () => { - axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notification marked read.' }); - - const { data } = await markNotificationRead(1); - - expect(data.message).toEqual('Notification marked read.'); - }); - - it.each([ - { statusCode: 404, message: 'Failed to mark notification as read.' }, - { statusCode: 403, message: 'Denied to mark notification as read.' }, - ])('%s for notification mark as read API.', async ({ statusCode, message }) => { - axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message }); - try { - await markAllNotificationRead(1); - } catch (error) { - expect(error.response.status).toEqual(statusCode); - expect(error.response.data.message).toEqual(message); - } - }); -}); diff --git a/src/Notifications/data/hook.js b/src/Notifications/data/hook.js deleted file mode 100644 index b41967a..0000000 --- a/src/Notifications/data/hook.js +++ /dev/null @@ -1,11 +0,0 @@ -import { breakpoints, useWindowSize } from '@edx/paragon'; - -export function useIsOnMediumScreen() { - const windowSize = useWindowSize(); - return breakpoints.large.maxWidth > windowSize.width && windowSize.width >= breakpoints.medium.minWidth; -} - -export function useIsOnLargeScreen() { - const windowSize = useWindowSize(); - return windowSize.width >= breakpoints.extraLarge.minWidth; -} diff --git a/src/Notifications/data/index.js b/src/Notifications/data/index.js deleted file mode 100644 index 4285022..0000000 --- a/src/Notifications/data/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './slice'; diff --git a/src/Notifications/data/notifications.json b/src/Notifications/data/notifications.json deleted file mode 100644 index 87e4eb6..0000000 --- a/src/Notifications/data/notifications.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "data": [ - { - "id": 1, - "type": "post", - "content": "

SCM_Lead posts Hello and welcome to SC0x!

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:46:11.979531Z" - }, - { - "id": 2, - "type": "help", - "content": "

MITx_Learner asked What grade does a student need to get in order to pass the course and earn a certificate?

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:36:11.979531Z" - }, - { - "id": 3, - "type": "post", - "content": "

SCM_Lead posts Hello and welcome to SC0x!

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:46:11.979531Z" - }, - { - "id": 4, - "type": "respond", - "content": "

MITx_Learner responded Can't find linear regression in section 3 review

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:36:11.979531Z" - }, - { - "id": 5, - "type": "comment", - "content": "

MITx_Learner commented on MITx_Expert's response on a post your following Can't find linear regression in section 3 review

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:36:11.979531Z" - }, - { - "id": 6, - "type": "question", - "content": "

MITx_Learner commented Examples of quadratic equations in supply chains

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:36:11.979531Z" - }, - { - "id": 7, - "type": "answer", - "content": "

MITx_Expert answered Examples of quadratic equations in supply chains

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-05T00:36:11.979531Z" - }, - { - "id": 8, - "type": "comment", - "content": "

MITx_Learner commented Examples of quadratic equations in supply chains

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:36:11.979531Z" - }, - { - "id": 9, - "type": "comment", - "content": "

MITx_Learner commented on MITx_Expert'swhat grade does a student need to get in order to pass the course and earn a certificate?

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:36:11.979531Z" - }, - { - "id": 10, - "type": "comment", - "content": "

MITx_Learner commented on your response in Convexity of f(x)=1/x , x>1

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:36:11.979531Z" - }, - { - "id": 11, - "type": "answer", - "content": "

SCM_Lead’s response has been marked as answer in your post Quiz in section 3 - Please explain the F-Significance value

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:36:11.979531Z" - }, - { - "id": 12, - "type": "endorsed", - "content": "

Your response has been endorsed in Quiz in section 3 - Please explain the F-Significance value

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:36:11.979531Z" - }, - { - "id": 13, - "type": "reported", - "content": "

MITx Learner’s post has been reported “Here are the exam answers. Question 1 - CSA stands for Compliance Safety Ac...”

", - "course_name": "Supply Chain Analytics", - "content_url": "", - "last_read": null, - "last_seen": null, - "created_at": "2023-06-01T00:36:11.979531Z" - } - ] -} diff --git a/src/Notifications/data/redux.test.js b/src/Notifications/data/redux.test.js deleted file mode 100644 index 3ceb0bf..0000000 --- a/src/Notifications/data/redux.test.js +++ /dev/null @@ -1,155 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { Factory } from 'rosie'; - -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { initializeMockApp } from '@edx/frontend-platform/testing'; - -import { initializeStore } from '../../store'; -import executeThunk from '../../test-utils'; -import mockNotificationsResponse from '../test-utils'; -import { - getNotificationsListApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, -} from './api'; -import { - fetchAppsNotificationCount, fetchNotificationList, markNotificationsAsRead, markAllNotificationsAsRead, - resetNotificationState, markNotificationsAsSeen, -} from './thunks'; - -import './__factories__'; - -const notificationCountsApiUrl = getNotificationsCountApiUrl(); -const notificationsListApiUrl = getNotificationsListApiUrl(); -const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl(); -const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussion'); - -let axiosMock; -let store; - -describe('Notification Redux', () => { - beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: '123abc', - username: 'testuser', - administrator: false, - roles: [], - }, - }); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - Factory.resetAll(); - store = initializeStore(); - - ({ store, axiosMock } = await mockNotificationsResponse()); - }); - - afterEach(() => { - axiosMock.reset(); - }); - - it('Successfully loaded initial notification states in the redux.', async () => { - executeThunk(resetNotificationState(), store.dispatch, store.getState); - - const { notifications } = store.getState(); - - expect(notifications.notificationStatus).toEqual('idle'); - expect(notifications.appName).toEqual('discussion'); - expect(notifications.appsId).toHaveLength(0); - expect(notifications.apps).toEqual({}); - expect(notifications.notifications).toEqual({}); - expect(notifications.tabsCount).toEqual({}); - expect(notifications.showNotificationsTray).toEqual(false); - expect(notifications.pagination).toEqual({}); - }); - - it('Successfully loaded notifications list in the redux.', async () => { - const { notifications: { notifications } } = store.getState(); - expect(Object.keys(notifications)).toHaveLength(10); - }); - - it.each([ - { statusCode: 404, status: 'failed' }, - { statusCode: 403, status: 'denied' }, - ])('%s to load notifications list in the redux.', async ({ statusCode, status }) => { - axiosMock.onGet(notificationsListApiUrl).reply(statusCode); - await executeThunk(fetchNotificationList({ page: 1 }), store.dispatch, store.getState); - - const { notifications: { notificationStatus } } = store.getState(); - - expect(notificationStatus).toEqual(status); - }); - - it('Successfully loaded notification counts in the redux.', async () => { - const { notifications: { tabsCount } } = store.getState(); - - expect(tabsCount.count).toEqual(25); - expect(tabsCount.reminders).toEqual(10); - expect(tabsCount.discussion).toEqual(0); - expect(tabsCount.grades).toEqual(10); - expect(tabsCount.authoring).toEqual(5); - }); - - it.each([ - { statusCode: 404, status: 'failed' }, - { statusCode: 403, status: 'denied' }, - ])('%s to load notification counts in the redux.', async ({ statusCode, status }) => { - axiosMock.onGet(notificationCountsApiUrl).reply(statusCode); - await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState); - - const { notifications: { notificationStatus } } = store.getState(); - - expect(notificationStatus).toEqual(status); - }); - - it('Successfully marked all notifications as seen for selected app.', async () => { - axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200); - await executeThunk(markNotificationsAsSeen('discussion'), store.dispatch, store.getState); - - expect(store.getState().notifications.notificationStatus).toEqual('successful'); - }); - - it.each([ - { statusCode: 404, status: 'failed' }, - { statusCode: 403, status: 'denied' }, - ])('%s to mark all notifications as seen for selected app.', async ({ statusCode, status }) => { - axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode); - await executeThunk(markNotificationsAsSeen('discussion'), store.dispatch, store.getState); - - const { notifications: { notificationStatus } } = store.getState(); - - expect(notificationStatus).toEqual(status); - }); - - it('Successfully marked all notifications as read for selected app in the redux.', async () => { - axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200); - await executeThunk(markAllNotificationsAsRead('discussion'), store.dispatch, store.getState); - - const { notifications: { notificationStatus, notifications } } = store.getState(); - const firstNotification = Object.values(notifications)[0]; - - expect(notificationStatus).toEqual('successful'); - expect(firstNotification.lastRead).not.toBeNull(); - }); - - it('Successfully marked notification as read in the redux.', async () => { - axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200); - await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState); - - const { notifications: { notificationStatus, notifications } } = store.getState(); - const firstNotification = Object.values(notifications)[0]; - - expect(notificationStatus).toEqual('successful'); - expect(firstNotification.lastRead).not.toBeNull(); - }); - - it.each([ - { statusCode: 404, status: 'failed' }, - { statusCode: 403, status: 'denied' }, - ])('%s to marked notification as read in the redux.', async ({ statusCode, status }) => { - axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode); - await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState); - - const { notifications: { notificationStatus } } = store.getState(); - - expect(notificationStatus).toEqual(status); - }); -}); diff --git a/src/Notifications/data/selector.test.jsx b/src/Notifications/data/selector.test.jsx deleted file mode 100644 index 31f2c80..0000000 --- a/src/Notifications/data/selector.test.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { Factory } from 'rosie'; - -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { initializeMockApp } from '@edx/frontend-platform/testing'; - -import { initializeStore } from '../../store'; -import mockNotificationsResponse from '../test-utils'; -import { - selectNotifications, - selectNotificationsByIds, - selectNotificationStatus, - selectNotificationTabs, - selectNotificationTabsCount, - selectPaginationData, - selectSelectedAppName, - selectSelectedAppNotificationIds, - selectShowNotificationTray, -} from './selectors'; - -import './__factories__'; - -let axiosMock; -let store; - -describe('Notification Selectors', () => { - beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: '123abc', - username: 'testuser', - administrator: false, - roles: [], - }, - }); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - Factory.resetAll(); - store = initializeStore(); - - ({ store, axiosMock } = await mockNotificationsResponse()); - }); - - afterEach(() => { - axiosMock.reset(); - }); - - it('Should return notification status.', async () => { - const state = store.getState(); - const status = selectNotificationStatus()(state); - - expect(status).toEqual('successful'); - }); - - it('Should return notification tabs count.', async () => { - const state = store.getState(); - const tabsCount = selectNotificationTabsCount()(state); - - expect(tabsCount.count).toEqual(25); - expect(tabsCount.reminders).toEqual(10); - expect(tabsCount.discussion).toEqual(0); - expect(tabsCount.grades).toEqual(10); - expect(tabsCount.authoring).toEqual(5); - }); - - it('Should return notification tabs.', async () => { - const state = store.getState(); - const tabs = selectNotificationTabs()(state); - - expect(tabs).toHaveLength(4); - }); - - it('Should return selected app notification ids.', async () => { - const state = store.getState(); - const notificationIds = selectSelectedAppNotificationIds('discussion')(state); - - expect(notificationIds).toHaveLength(10); - }); - - it('Should return show notification tray status.', async () => { - const state = store.getState(); - const showNotificationTrayStatus = selectShowNotificationTray()(state); - - expect(showNotificationTrayStatus).toEqual(true); - }); - - it('Should return notifications.', async () => { - const state = store.getState(); - const notifications = selectNotifications()(state); - - expect(Object.keys(notifications)).toHaveLength(10); - }); - - it('Should return notifications from Ids.', async () => { - const state = store.getState(); - const notifications = selectNotificationsByIds('discussion')(state); - - expect(notifications).toHaveLength(10); - }); - - it('Should return selected app name.', async () => { - const state = store.getState(); - const appName = selectSelectedAppName()(state); - - expect(appName).toEqual('discussion'); - }); - - it('Should return pagination data.', async () => { - const state = store.getState(); - const paginationData = selectPaginationData()(state); - - expect(paginationData.currentPage).toEqual(1); - expect(paginationData.numPages).toEqual(2); - expect(paginationData.hasMorePages).toEqual(true); - }); -}); diff --git a/src/Notifications/data/selectors.js b/src/Notifications/data/selectors.js deleted file mode 100644 index 151c4f1..0000000 --- a/src/Notifications/data/selectors.js +++ /dev/null @@ -1,23 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; - -export const selectNotificationStatus = () => state => state.notifications.notificationStatus; - -export const selectNotificationTabsCount = () => state => state.notifications.tabsCount; - -export const selectNotificationTabs = () => state => state.notifications.appsId; - -export const selectSelectedAppNotificationIds = (appName) => state => state.notifications.apps[appName] ?? []; - -export const selectShowNotificationTray = () => state => state.notifications.showNotificationsTray; - -export const selectNotifications = () => state => state.notifications.notifications; - -export const selectNotificationsByIds = (appName) => createSelector( - selectNotifications(), - selectSelectedAppNotificationIds(appName), - (notifications, notificationIds) => notificationIds.map((notificationId) => notifications[notificationId]) || [], -); - -export const selectSelectedAppName = () => state => state.notifications.appName; - -export const selectPaginationData = () => state => state.notifications.pagination; diff --git a/src/Notifications/data/slice.js b/src/Notifications/data/slice.js deleted file mode 100644 index 6ee4178..0000000 --- a/src/Notifications/data/slice.js +++ /dev/null @@ -1,147 +0,0 @@ -/* eslint-disable no-param-reassign */ -import { createSlice } from '@reduxjs/toolkit'; - -export const RequestStatus = { - IDLE: 'idle', - IN_PROGRESS: 'in-progress', - SUCCESSFUL: 'successful', - FAILED: 'failed', - DENIED: 'denied', -}; - -const initialState = { - notificationStatus: RequestStatus.IDLE, - appName: 'discussion', - appsId: [], - apps: {}, - notifications: {}, - tabsCount: {}, - showNotificationsTray: false, - pagination: {}, -}; -const slice = createSlice({ - name: 'notifications', - initialState, - reducers: { - fetchNotificationDenied: (state) => { - state.notificationStatus = RequestStatus.DENIED; - }, - fetchNotificationFailure: (state) => { - state.notificationStatus = RequestStatus.FAILED; - }, - fetchNotificationRequest: (state) => { - state.notificationStatus = RequestStatus.IN_PROGRESS; - }, - fetchNotificationSuccess: (state, { payload }) => { - const { - newNotificationIds, notificationsKeyValuePair, pagination, - } = payload; - const existingNotificationIds = state.apps[state.appName]; - state.apps[state.appName] = Array.from(new Set([...existingNotificationIds, ...newNotificationIds])); - state.notifications = { ...state.notifications, ...notificationsKeyValuePair }; - state.tabsCount.count -= state.tabsCount[state.appName]; - state.tabsCount[state.appName] = 0; - state.notificationStatus = RequestStatus.SUCCESSFUL; - state.pagination = pagination; - }, - fetchNotificationsCountDenied: (state) => { - state.notificationStatus = RequestStatus.DENIED; - }, - fetchNotificationsCountFailure: (state) => { - state.notificationStatus = RequestStatus.FAILED; - }, - fetchNotificationsCountRequest: (state) => { - state.notificationStatus = RequestStatus.IN_PROGRESS; - }, - fetchNotificationsCountSuccess: (state, { payload }) => { - const { - countByAppName, appIds, apps, count, showNotificationsTray, - } = payload; - state.tabsCount = { count, ...countByAppName }; - state.appsId = appIds; - state.apps = apps; - state.showNotificationsTray = showNotificationsTray; - state.notificationStatus = RequestStatus.SUCCESSFUL; - }, - markNotificationsAsSeenRequest: (state) => { - state.notificationStatus = RequestStatus.IN_PROGRESS; - }, - markNotificationsAsSeenSuccess: (state) => { - state.notificationStatus = RequestStatus.SUCCESSFUL; - }, - markNotificationsAsSeenDenied: (state) => { - state.notificationStatus = RequestStatus.DENIED; - }, - markNotificationsAsSeenFailure: (state) => { - state.notificationStatus = RequestStatus.FAILED; - }, - markAllNotificationsAsReadRequest: (state) => { - state.notificationStatus = RequestStatus.IN_PROGRESS; - }, - markAllNotificationsAsReadSuccess: (state) => { - const updatedNotifications = Object.fromEntries( - Object.entries(state.notifications).map(([key, notification]) => [ - key, { ...notification, lastRead: new Date().toISOString() }, - ]), - ); - state.notifications = updatedNotifications; - state.notificationStatus = RequestStatus.SUCCESSFUL; - }, - markAllNotificationsAsReadDenied: (state) => { - state.notificationStatus = RequestStatus.DENIED; - }, - markAllNotificationsAsReadFailure: (state) => { - state.notificationStatus = RequestStatus.FAILED; - }, - markNotificationsAsReadRequest: (state) => { - state.notificationStatus = RequestStatus.IN_PROGRESS; - }, - markNotificationsAsReadSuccess: (state, { payload }) => { - const date = new Date().toISOString(); - state.notifications[payload.id] = { ...state.notifications[payload.id], lastRead: date }; - state.notificationStatus = RequestStatus.SUCCESSFUL; - }, - markNotificationsAsReadDenied: (state) => { - state.notificationStatus = RequestStatus.DENIED; - }, - markNotificationsAsReadFailure: (state) => { - state.notificationStatus = RequestStatus.FAILED; - }, - resetNotificationStateRequest: () => initialState, - updateAppNameRequest: (state, { payload }) => { - state.appName = payload.appName; - state.pagination.currentPage = 1; - }, - updatePaginationRequest: (state) => { - state.pagination.currentPage += 1; - }, - }, -}); - -export const { - fetchNotificationDenied, - fetchNotificationFailure, - fetchNotificationRequest, - fetchNotificationSuccess, - fetchNotificationsCountDenied, - fetchNotificationsCountFailure, - fetchNotificationsCountRequest, - fetchNotificationsCountSuccess, - markNotificationsAsSeenRequest, - markNotificationsAsSeenSuccess, - markNotificationsAsSeenFailure, - markNotificationsAsSeenDenied, - markAllNotificationsAsReadDenied, - markAllNotificationsAsReadRequest, - markAllNotificationsAsReadSuccess, - markAllNotificationsAsReadFailure, - markNotificationsAsReadDenied, - markNotificationsAsReadRequest, - markNotificationsAsReadSuccess, - markNotificationsAsReadFailure, - resetNotificationStateRequest, - updateAppNameRequest, - updatePaginationRequest, -} = slice.actions; - -export const notificationsReducer = slice.reducer; diff --git a/src/Notifications/data/thunks.js b/src/Notifications/data/thunks.js deleted file mode 100644 index 585ce0e..0000000 --- a/src/Notifications/data/thunks.js +++ /dev/null @@ -1,135 +0,0 @@ -import { camelCaseObject } from '@edx/frontend-platform'; -import { - fetchNotificationSuccess, - fetchNotificationRequest, - fetchNotificationFailure, - fetchNotificationDenied, - fetchNotificationsCountFailure, - fetchNotificationsCountRequest, - fetchNotificationsCountSuccess, - fetchNotificationsCountDenied, - markNotificationsAsSeenRequest, - markNotificationsAsSeenSuccess, - markNotificationsAsSeenFailure, - markNotificationsAsSeenDenied, - markNotificationsAsReadDenied, - resetNotificationStateRequest, - markAllNotificationsAsReadRequest, - markAllNotificationsAsReadSuccess, - markAllNotificationsAsReadFailure, - markAllNotificationsAsReadDenied, - markNotificationsAsReadRequest, - markNotificationsAsReadSuccess, - markNotificationsAsReadFailure, -} from './slice'; -import { - getNotificationsList, getNotificationCounts, markNotificationSeen, markAllNotificationRead, markNotificationRead, -} from './api'; -import { getHttpErrorStatus } from '../utils'; - -const normalizeNotificationCounts = ({ countByAppName, count, showNotificationsTray }) => { - const appIds = Object.keys(countByAppName); - const apps = appIds.reduce((acc, appId) => { acc[appId] = []; return acc; }, {}); - return { - countByAppName, appIds, apps, count, showNotificationsTray, - }; -}; - -const normalizeNotifications = (data) => { - const newNotificationIds = data.results.map(notification => notification.id.toString()); - const notificationsKeyValuePair = data.results.reduce((acc, obj) => { acc[obj.id] = obj; return acc; }, {}); - const pagination = { - numPages: data.numPages, - currentPage: data.currentPage, - hasMorePages: !!data.next, - }; - return { - newNotificationIds, notificationsKeyValuePair, pagination, - }; -}; - -export const fetchNotificationList = ({ appName, page }) => ( - async (dispatch) => { - try { - dispatch(fetchNotificationRequest({ appName })); - const data = await getNotificationsList(appName, page); - const normalisedData = normalizeNotifications((camelCaseObject(data))); - dispatch(fetchNotificationSuccess({ ...normalisedData })); - } catch (error) { - if (getHttpErrorStatus(error) === 403) { - dispatch(fetchNotificationDenied(appName)); - } else { - dispatch(fetchNotificationFailure(appName)); - } - } - } -); - -export const fetchAppsNotificationCount = () => ( - async (dispatch) => { - try { - dispatch(fetchNotificationsCountRequest()); - const data = await getNotificationCounts(); - const normalisedData = normalizeNotificationCounts((camelCaseObject(data))); - dispatch(fetchNotificationsCountSuccess({ ...normalisedData })); - } catch (error) { - if (getHttpErrorStatus(error) === 403) { - dispatch(fetchNotificationsCountDenied()); - } else { - dispatch(fetchNotificationsCountFailure()); - } - } - } -); - -export const markAllNotificationsAsRead = (appName) => ( - async (dispatch) => { - try { - dispatch(markAllNotificationsAsReadRequest({ appName })); - const data = await markAllNotificationRead(appName); - dispatch(markAllNotificationsAsReadSuccess(camelCaseObject(data))); - } catch (error) { - if (getHttpErrorStatus(error) === 403) { - dispatch(markAllNotificationsAsReadDenied()); - } else { - dispatch(markAllNotificationsAsReadFailure()); - } - } - } -); - -export const markNotificationsAsRead = (notificationId) => ( - async (dispatch) => { - try { - dispatch(markNotificationsAsReadRequest({ notificationId })); - const data = await markNotificationRead(notificationId); - dispatch(markNotificationsAsReadSuccess(camelCaseObject(data))); - } catch (error) { - if (getHttpErrorStatus(error) === 403) { - dispatch(markNotificationsAsReadDenied()); - } else { - dispatch(markNotificationsAsReadFailure()); - } - } - } -); - -export const markNotificationsAsSeen = (appName) => ( - async (dispatch) => { - try { - dispatch(markNotificationsAsSeenRequest({ appName })); - const data = await markNotificationSeen(appName); - dispatch(markNotificationsAsSeenSuccess(camelCaseObject(data))); - } catch (error) { - if (getHttpErrorStatus(error) === 403) { - dispatch(markNotificationsAsSeenDenied()); - } else { - dispatch(markNotificationsAsSeenFailure()); - } - } - } -); - -export const resetNotificationState = () => ( - async (dispatch) => { dispatch(resetNotificationStateRequest()); } -); diff --git a/src/Notifications/index.jsx b/src/Notifications/index.jsx deleted file mode 100644 index 31b3aa2..0000000 --- a/src/Notifications/index.jsx +++ /dev/null @@ -1,103 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import React, { - useCallback, useEffect, useRef, useState, -} from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import classNames from 'classnames'; -import { - Badge, Icon, IconButton, OverlayTrigger, Popover, -} from '@edx/paragon'; -import { NotificationsNone, Settings } from '@edx/paragon/icons'; -import { selectNotificationTabsCount } from './data/selectors'; -import { resetNotificationState } from './data/thunks'; -import { useIsOnLargeScreen, useIsOnMediumScreen } from './data/hook'; -import NotificationTabs from './NotificationTabs'; -import messages from './messages'; - -const Notifications = () => { - const intl = useIntl(); - const dispatch = useDispatch(); - const popoverRef = useRef(null); - const buttonRef = useRef(null); - const [enableNotificationTray, setEnableNotificationTray] = useState(false); - const notificationCounts = useSelector(selectNotificationTabsCount()); - const isOnMediumScreen = useIsOnMediumScreen(); - const isOnLargeScreen = useIsOnLargeScreen(); - - const hideNotificationTray = useCallback(() => { - setEnableNotificationTray(prevState => !prevState); - }, []); - - const handleClickOutsideNotificationTray = useCallback((event) => { - if (!popoverRef.current?.contains(event.target) && !buttonRef.current?.contains(event.target)) { - setEnableNotificationTray(false); - } - }, []); - - useEffect(() => { - document.addEventListener('mousedown', handleClickOutsideNotificationTray); - - return () => { - document.removeEventListener('mousedown', handleClickOutsideNotificationTray); - dispatch(resetNotificationState()); - }; - }, []); - - return ( - -
- - {intl.formatMessage(messages.notificationTitle)} - - - - - -
- - )} - > -
- - {notificationCounts?.count > 0 && ( - - {notificationCounts.count} - - )} -
-
- ); -}; - -export default Notifications; diff --git a/src/Notifications/index.test.jsx b/src/Notifications/index.test.jsx deleted file mode 100644 index b0b5cc5..0000000 --- a/src/Notifications/index.test.jsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; - -import { - act, fireEvent, render, screen, waitFor, -} from '@testing-library/react'; -import MockAdapter from 'axios-mock-adapter'; -import { Context as ResponsiveContext } from 'react-responsive'; -import { Factory } from 'rosie'; - -import { initializeMockApp } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { AppContext, AppProvider } from '@edx/frontend-platform/react'; - -import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown'; -import { initializeStore } from '../store'; -import executeThunk from '../test-utils'; -import { getNotificationsCountApiUrl } from './data/api'; -import { fetchAppsNotificationCount } from './data/thunks'; - -import './data/__factories__'; - -const notificationCountsApiUrl = getNotificationsCountApiUrl(); - -let axiosMock; -let store; - -function renderComponent() { - render( - - - - - - - - - , - ); -} - -describe('Notification test cases.', () => { - beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: '123abc', - username: 'testuser', - administrator: false, - roles: [], - }, - }); - - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - Factory.resetAll(); - store = initializeStore(); - }); - - async function setupMockNotificationCountResponse(count = 45, showNotificationsTray = true) { - axiosMock.onGet(notificationCountsApiUrl) - .reply(200, (Factory.build('notificationsCount', { count, showNotificationsTray }))); - await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState); - } - - it('Successfully showed bell icon and unseen count on it if unseen count is greater then 0.', async () => { - await setupMockNotificationCountResponse(); - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - const notificationCount = screen.queryByTestId('notification-count'); - - expect(bellIcon).toBeInTheDocument(); - expect(notificationCount).toBeInTheDocument(); - expect(screen.queryByText(45)).toBeInTheDocument(); - }); - - it('Successfully showed bell icon and hide unseen count tag when unseen count is zero.', async () => { - await setupMockNotificationCountResponse(0); - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - const notificationCount = screen.queryByTestId('notification-count'); - - expect(bellIcon).toBeInTheDocument(); - expect(notificationCount).not.toBeInTheDocument(); - }); - - it('Successfully hides bell icon when showNotificationsTray is false.', async () => { - await setupMockNotificationCountResponse(45, false); - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - - expect(bellIcon).not.toBeInTheDocument(); - }); - - it('Successfully viewed setting icon and show/hide notification tray by clicking on the bell icon .', async () => { - await setupMockNotificationCountResponse(); - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - - await act(async () => { fireEvent.click(bellIcon); }); - expect(screen.queryByTestId('notification-tray')).toBeInTheDocument(); - expect(screen.queryByTestId('setting-icon')).toBeInTheDocument(); - - await act(async () => { fireEvent.click(bellIcon); }); - await waitFor(() => expect(screen.queryByTestId('notification-tray')).not.toBeInTheDocument()); - }); -}); diff --git a/src/Notifications/messages.js b/src/Notifications/messages.js deleted file mode 100644 index a26ceed..0000000 --- a/src/Notifications/messages.js +++ /dev/null @@ -1,36 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - notificationTitle: { - id: 'notification.title', - defaultMessage: 'Notifications', - description: 'Notifications', - }, - notificationTodayHeading: { - id: 'notification.today.heading', - defaultMessage: 'Last 24 hours', - description: 'Today Notifications', - }, - notificationEarlierHeading: { - id: 'notification.earlier.heading', - defaultMessage: 'Earlier', - description: 'Earlier Notifications', - }, - notificationMarkAsRead: { - id: 'notification.mark.as.read', - defaultMessage: 'Mark all as read', - description: 'Mark all Notifications as read', - }, - fullStop: { - id: 'notification.fullStop', - defaultMessage: '•', - description: 'Fullstop shown to users to indicate who edited a post.', - }, - loadMoreNotifications: { - id: 'notification.load.more.notifications', - defaultMessage: 'Load more notifications', - description: 'Load more button to load more notifications', - }, -}); - -export default messages; diff --git a/src/Notifications/notificationRowItem.test.jsx b/src/Notifications/notificationRowItem.test.jsx deleted file mode 100644 index 332a026..0000000 --- a/src/Notifications/notificationRowItem.test.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; - -import { - act, fireEvent, render, screen, -} from '@testing-library/react'; -import MockAdapter from 'axios-mock-adapter'; -import { Context as ResponsiveContext } from 'react-responsive'; -import { Factory } from 'rosie'; - -import { initializeMockApp } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { AppContext, AppProvider } from '@edx/frontend-platform/react'; - -import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown'; -import { initializeStore } from '../store'; -import { markNotificationAsReadApiUrl } from './data/api'; -import mockNotificationsResponse from './test-utils'; - -import './data/__factories__'; - -const markedNotificationAsReadApiUrl = markNotificationAsReadApiUrl(); - -let axiosMock; -let store; - -function renderComponent() { - render( - - - - - - - - - , - ); -} - -describe('Notification row item test cases.', () => { - beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - Factory.resetAll(); - store = initializeStore(); - - ({ store, axiosMock } = await mockNotificationsResponse()); - }); - - it( - 'Successfully viewed notification icon, notification context, unread , course name and notification time.', - async () => { - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - await act(async () => { fireEvent.click(bellIcon); }); - - expect(screen.queryByTestId('notification-icon-1')).toBeInTheDocument(); - expect(screen.queryByTestId('notification-content-1')).toBeInTheDocument(); - expect(screen.queryByTestId('notification-course-1')).toBeInTheDocument(); - expect(screen.queryByTestId('notification-created-date-1')).toBeInTheDocument(); - expect(screen.queryByTestId('unread-notification-1')).toBeInTheDocument(); - }, - ); - - it('Successfully marked notification as read.', async () => { - axiosMock.onPatch(markedNotificationAsReadApiUrl).reply(200, { message: 'Notification marked read.' }); - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - await act(async () => { fireEvent.click(bellIcon); }); - - const notification = screen.queryByTestId('notification-1'); - await act(async () => { fireEvent.click(notification); }); - - expect(screen.queryByTestId('unread-notification-1')).not.toBeInTheDocument(); - }); -}); diff --git a/src/Notifications/notificationSections.test.jsx b/src/Notifications/notificationSections.test.jsx deleted file mode 100644 index 1495fac..0000000 --- a/src/Notifications/notificationSections.test.jsx +++ /dev/null @@ -1,106 +0,0 @@ -import React from 'react'; - -import { - act, fireEvent, render, screen, within, -} from '@testing-library/react'; -import MockAdapter from 'axios-mock-adapter'; -import { Context as ResponsiveContext } from 'react-responsive'; -import { Factory } from 'rosie'; - -import { initializeMockApp } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { AppContext, AppProvider } from '@edx/frontend-platform/react'; - -import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown'; -import { initializeStore } from '../store'; -import { markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, getNotificationsListApiUrl } from './data/api'; -import mockNotificationsResponse from './test-utils'; -import { markNotificationsAsSeen, fetchNotificationList } from './data/thunks'; -import executeThunk from '../test-utils'; -import './data/__factories__'; - -const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl(); - -let axiosMock; -let store; - -function renderComponent() { - render( - - - - - - - - - , - ); -} - -describe('Notification sections test cases.', () => { - beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - Factory.resetAll(); - store = initializeStore(); - - ({ store, axiosMock } = await mockNotificationsResponse()); - }); - - it('Successfully viewed last 24 hours and earlier section along with mark all as read label.', async () => { - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - await act(async () => { fireEvent.click(bellIcon); }); - const notificationTraySection = screen.queryByTestId('notification-tray-section'); - - expect(within(notificationTraySection).queryByText('Last 24 hours')).toBeInTheDocument(); - expect(within(notificationTraySection).queryByText('Earlier')).toBeInTheDocument(); - expect(within(notificationTraySection).queryByText('Mark all as read')).toBeInTheDocument(); - }); - - it('Successfully marked all notifications as read, removing the unread status.', async () => { - axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' }); - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - await act(async () => { fireEvent.click(bellIcon); }); - const markAllReadButton = screen.queryByTestId('mark-all-read'); - - expect(screen.queryByTestId('unread-notification-1')).toBeInTheDocument(); - await act(async () => { fireEvent.click(markAllReadButton); }); - - expect(screen.queryByTestId('unread-notification-1')).not.toBeInTheDocument(); - }); - - it('Successfully load more notifications by clicking on load more notification button.', async () => { - axiosMock.onPut(markNotificationsSeenApiUrl('discussion')).reply(200); - await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState); - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - await act(async () => { fireEvent.click(bellIcon); }); - - expect(screen.queryAllByTestId('notification-contents')).toHaveLength(10); - const loadMoreButton = screen.queryByTestId('load-more-notifications'); - - axiosMock.onGet(getNotificationsListApiUrl()).reply( - 200, - (Factory.build('notificationsList', { num_pages: 2, current_page: 2 })), - ); - await executeThunk(fetchNotificationList({ appName: 'discussion', page: 2 }), store.dispatch, store.getState); - - await act(async () => { fireEvent.click(loadMoreButton); }); - expect(screen.queryAllByTestId('notification-contents')).toHaveLength(12); - }); -}); diff --git a/src/Notifications/notificationTabs.test.jsx b/src/Notifications/notificationTabs.test.jsx deleted file mode 100644 index 50e1e63..0000000 --- a/src/Notifications/notificationTabs.test.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; - -import { - act, fireEvent, render, screen, within, -} from '@testing-library/react'; -import { Context as ResponsiveContext } from 'react-responsive'; -import { Factory } from 'rosie'; - -import { initializeMockApp } from '@edx/frontend-platform'; -import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { AppContext, AppProvider } from '@edx/frontend-platform/react'; - -import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown'; -import { initializeStore } from '../store'; -import mockNotificationsResponse from './test-utils'; - -import './data/__factories__'; - -let store; - -function renderComponent() { - render( - - - - - - - - - , - ); -} - -describe('Notification Tabs test cases.', () => { - beforeEach(async () => { - initializeMockApp({ - authenticatedUser: { - userId: '123abc', - username: 'testuser', - administrator: false, - roles: [], - }, - }); - - Factory.resetAll(); - store = initializeStore(); - - ({ store } = await mockNotificationsResponse()); - }); - - it('Notification tabs displayed with default discussion tab selected and no unseen counts.', async () => { - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - await act(async () => { fireEvent.click(bellIcon); }); - - const tabs = screen.queryAllByRole('tab'); - const selectedTab = tabs.find(tab => tab.getAttribute('aria-selected') === 'true'); - - expect(tabs.length).toEqual(5); - expect(within(selectedTab).queryByText('discussion')).toBeInTheDocument(); - expect(within(selectedTab).queryByRole('status')).not.toBeInTheDocument(); - }); - - it('Successfully showed unseen counts for unselected tabs.', async () => { - renderComponent(); - const bellIcon = screen.queryByTestId('notification-bell-icon'); - await act(async () => { fireEvent.click(bellIcon); }); - - const tabs = screen.getAllByRole('tab'); - - expect(within(tabs[0]).queryByRole('status')).toBeInTheDocument(); - }); - - it('Successfully selected reminder tab.', async () => { - renderComponent(); - - const bellIcon = screen.queryByTestId('notification-bell-icon'); - await act(async () => { fireEvent.click(bellIcon); }); - const notificationTab = screen.getAllByRole('tab'); - - await act(async () => { fireEvent.click(notificationTab[0], { dataset: { rbEventKey: 'reminders' } }); }); - - const tabs = screen.queryAllByRole('tab'); - const selectedTab = tabs.find(tab => tab.getAttribute('aria-selected') === 'true'); - - expect(within(selectedTab).queryByText('reminders')).toBeInTheDocument(); - expect(within(selectedTab).queryByRole('status')).not.toBeInTheDocument(); - }); -}); diff --git a/src/Notifications/test-utils.js b/src/Notifications/test-utils.js deleted file mode 100644 index c0dcd58..0000000 --- a/src/Notifications/test-utils.js +++ /dev/null @@ -1,32 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { Factory } from 'rosie'; - -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; - -import { initializeStore } from '../store'; -import executeThunk from '../test-utils'; -import { getNotificationsListApiUrl, getNotificationsCountApiUrl } from './data/api'; -import { fetchAppsNotificationCount, fetchNotificationList } from './data/thunks'; - -import './data/__factories__'; - -const notificationCountsApiUrl = getNotificationsCountApiUrl(); -const notificationsApiUrl = getNotificationsListApiUrl(); - -export default async function mockNotificationsResponse() { - const store = initializeStore(); - const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - const notifications = (Factory.buildList('notification', 8, null, { createdDate: new Date().toISOString() }).concat( - Factory.buildList('notification', 2, null, { createdDate: '2023-06-01T00:46:11.979531Z' }), - )); - axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); - axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.build('notificationsList', { - results: notifications, - num_pages: 2, - next: `${notificationsApiUrl}?app_name=discussion&page=2`, - }))); - - await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState); - await executeThunk(fetchNotificationList({ appName: 'discussion', page: 1 }), store.dispatch, store.getState); - return { store, axiosMock }; -} diff --git a/src/Notifications/utils.js b/src/Notifications/utils.js deleted file mode 100644 index a0e99d2..0000000 --- a/src/Notifications/utils.js +++ /dev/null @@ -1,52 +0,0 @@ -import { - CheckCircle, HelpOutline, QuestionAnswerOutline, Verified, Report, EditOutline, ThumbUpOutline, PostOutline, -} from '@edx/paragon/icons'; - -/** - * Get HTTP Error status from generic error. - * @param error Generic caught error. - * @returns {number|null} - */ -export const getHttpErrorStatus = error => error?.customAttributes?.httpErrorStatus ?? error?.response?.status; - -export const splitNotificationsByTime = (notificationList) => { - let splittedData = []; - if (notificationList.length > 0) { - const currentTime = Date.now(); - const twentyFourHoursAgo = currentTime - (24 * 60 * 60 * 1000); - - splittedData = notificationList.reduce( - (result, notification) => { - if (notification) { - const objectTime = new Date(notification.createdAt).getTime(); - if (objectTime >= twentyFourHoursAgo && objectTime <= currentTime) { - result.today.push(notification); - } else { - result.earlier.push(notification); - } - } - return result; - }, - { today: [], earlier: [] }, - ); - } - const { today, earlier } = splittedData; - return { today, earlier }; -}; - -export const getIconByType = (type) => { - const iconMap = { - post: { icon: PostOutline, class: 'text-primary-500' }, - help: { icon: HelpOutline, class: 'text-primary-500' }, - respond: { icon: QuestionAnswerOutline, class: 'text-primary-500' }, - comment: { icon: QuestionAnswerOutline, class: 'text-primary-500' }, - question: { icon: QuestionAnswerOutline, class: 'text-primary-500' }, - answer: { icon: CheckCircle, class: 'text-success' }, - endorsed: { icon: Verified, class: 'text-primary-500' }, - reported: { icon: Report, class: 'text-danger-500' }, - postLiked: { icon: ThumbUpOutline, class: 'text-primary-500' }, - commentLiked: { icon: ThumbUpOutline, class: 'text-primary-500' }, - edited: { icon: EditOutline, class: 'text-primary-500' }, - }; - return iconMap[type] || { icon: PostOutline, class: 'text-primary-500' }; -}; diff --git a/src/common/time-locale.js b/src/common/time-locale.js deleted file mode 100644 index 4a618dd..0000000 --- a/src/common/time-locale.js +++ /dev/null @@ -1,18 +0,0 @@ -export default function timeLocale(number, index) { - return [ - ['just now', 'right now'], - ['%ss', 'in %s seconds'], - ['1m', 'in 1 minute'], - ['%sm', 'in %s minutes'], - ['1h', 'in 1 hour'], - ['%sh', 'in %s hours'], - ['1d', 'in 1 day'], - ['%sd', 'in %s days'], - ['1w', 'in 1 week'], - ['%sw', 'in %s weeks'], - ['4w', 'in 1 month'], - [`${number * 4}w`, 'in %s months'], - ['1y', 'in 1 year'], - ['%sy', 'in %s years'], - ][index]; -} diff --git a/src/index.scss b/src/index.scss index 46ed12d..f6d2314 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,10 +1,7 @@ $spacer: 1rem; $blue: #007db8; $white: #fff; -@import "@edx/brand/paragon/fonts.scss"; -@import "@edx/brand/paragon/variables.scss"; -@import "@edx/paragon/scss/core/core.scss"; -@import "@edx/brand/paragon/overrides.scss"; + @import './Menu/menu.scss'; .dropdown-item a { @@ -121,135 +118,3 @@ $white: #fff; border-radius: $rounded-pill; } } - -.content { - strong { - color: #00262B !important; - font-weight: 500 !important; - } -} - -.font-size-18 { - font-size: 18px !important; -} - -.font-size-12 { - font-size: 12px; -} - -.font-size-14 { - font-size: 14px; -} - -.py-10px { - padding-top: 10px; - padding-bottom: 10px; -} - -.pb-10px { - padding-bottom: 10px; -} - -.line-height-24 { - line-height: 24px; -} - -.line-height-20 { - line-height: 20px; -} - -.line-height-10 { - line-height: 10px !important; -} - -.icon-size-20 { - width: 20px !important; - height: 20px !important; -} - -.cursor-pointer { - cursor: pointer; -} - -.notification-button { - width: 36px; - height: 36px; -} - -.notification-icon{ - height: 23.33px !important; - width: 23.33px !important; -} - -.notification-badge { - position: absolute; - margin-top: 18px; - margin-left: -21px; - border: 2px solid #FFFFFF; - font-size: 9px !important; -} - -.popover { - max-height: calc(100% - 68px); - min-height: 1220px; - filter: none; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15), 0px 2px 8px rgba(0, 0, 0, 0.15); - - &.medium-screen { - min-width: 24.313rem; - } - - &.large-screen { - min-width: 34.313rem; - } - - .dropdown-toggle::after { - display: none; - } - - .expandable { - position: relative !important; - margin-left: 4px; - padding: 2px 5px; - border-radius: 10rem; - font-size: 9px; - } - - .dropdown-toggle { - font-size: 14px; - padding-top: 0px !important; - padding-bottom: 12px !important; - - div { - min-height: 6px !important; - min-width: 6px !important; - } - } - - .dropdown-item { - font-size: 14px; - font-weight: 500; - } - - .notification-content { - .notification-item-content { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - text-overflow: ellipsis; - - p { - margin-bottom: 0px; - } - - b { - color: #00262B; - } - } - - .unread { - height: 10px; - width: 10px; - } - } -} diff --git a/src/learning-header/AuthenticatedUserDropdown.jsx b/src/learning-header/AuthenticatedUserDropdown.jsx index 889ff3f..4090bea 100644 --- a/src/learning-header/AuthenticatedUserDropdown.jsx +++ b/src/learning-header/AuthenticatedUserDropdown.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -6,26 +6,10 @@ import { faUserCircle } from '@fortawesome/free-solid-svg-icons'; import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Dropdown } from '@edx/paragon'; -import { useSelector, useDispatch } from 'react-redux'; -import Notifications from '../Notifications'; -import { selectShowNotificationTray, selectNotificationStatus } from '../Notifications/data/selectors'; -import { fetchAppsNotificationCount } from '../Notifications/data/thunks'; -import { RequestStatus } from '../Notifications/data/slice'; import messages from './messages'; const AuthenticatedUserDropdown = ({ intl, username }) => { - const showNotificationsTray = useSelector(selectShowNotificationTray()); - const notificationStatus = useSelector(selectNotificationStatus()); - const dispatch = useDispatch(); - - useEffect(() => { - if (notificationStatus === RequestStatus.IDLE) { - dispatch(fetchAppsNotificationCount()); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const dashboardMenuItem = ( {intl.formatMessage(messages.dashboard)} @@ -35,7 +19,6 @@ const AuthenticatedUserDropdown = ({ intl, username }) => { return ( <> {intl.formatMessage(messages.help)} - {showNotificationsTray && } diff --git a/src/learning-header/LearningHeader.jsx b/src/learning-header/LearningHeader.jsx index 32356f0..373001d 100644 --- a/src/learning-header/LearningHeader.jsx +++ b/src/learning-header/LearningHeader.jsx @@ -2,12 +2,11 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { AppContext, AppProvider } from '@edx/frontend-platform/react'; +import { AppContext } from '@edx/frontend-platform/react'; import AnonymousUserMenu from './AnonymousUserMenu'; import AuthenticatedUserDropdown from './AuthenticatedUserDropdown'; import messages from './messages'; -import store from '../store'; const LinkedLogo = ({ href, @@ -41,26 +40,24 @@ const LearningHeader = ({ ); return ( - -
- {intl.formatMessage(messages.skipNavLink)} -
- {headerLogo} -
- {courseOrg} {courseNumber} - {courseTitle} -
- {showUserDropdown && authenticatedUser && ( - - )} - {showUserDropdown && !authenticatedUser && ( - - )} +
+ {intl.formatMessage(messages.skipNavLink)} +
+ {headerLogo} +
+ {courseOrg} {courseNumber} + {courseTitle}
-
- + {showUserDropdown && authenticatedUser && ( + + )} + {showUserDropdown && !authenticatedUser && ( + + )} +
+
); }; diff --git a/src/store.js b/src/store.js deleted file mode 100644 index 342022c..0000000 --- a/src/store.js +++ /dev/null @@ -1,16 +0,0 @@ -import { configureStore } from '@reduxjs/toolkit'; - -import { notificationsReducer } from './Notifications/data'; - -export function initializeStore(preloadedState = undefined) { - return configureStore({ - reducer: { - notifications: notificationsReducer, - }, - preloadedState, - }); -} - -const store = initializeStore(); - -export default store;