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 }) => (
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 ( -