diff --git a/src/Notifications/NotificationSections.jsx b/src/Notifications/NotificationSections.jsx index f2fefc7..37d081e 100644 --- a/src/Notifications/NotificationSections.jsx +++ b/src/Notifications/NotificationSections.jsx @@ -1,21 +1,24 @@ import React, { useCallback, useMemo } from 'react'; -import { Button } from '@edx/paragon'; +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 } from './data/selectors'; +import { + selectNotificationsByIds, selectPaginationData, selectSelectedAppName, selectNotificationStatus, +} from './data/selectors'; import { splitNotificationsByTime } from './utils'; -import { updatePaginationRequest } from './data/slice'; +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 { currentPage, numPages } = useSelector(selectPaginationData()); + const { hasMorePages } = useSelector(selectPaginationData()); const { today = [], earlier = [] } = useMemo( () => splitNotificationsByTime(notifications), [notifications], @@ -70,15 +73,21 @@ const NotificationSections = () => {
User ${idx} posts Hello and welcome to SC0x - ${notificationId}!
`) + .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 index 020ef80..5d18f6b 100644 --- a/src/Notifications/data/api.js +++ b/src/Notifications/data/api.js @@ -2,19 +2,14 @@ import { getConfig, snakeCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; export const getNotificationsCountApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/count/`; -export const getNotificationsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`; -export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-notifications-unseen/${appName}/`; +export const 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 getNotifications(appName, page, pageSize) { - const params = snakeCaseObject({ page, pageSize }); - const { data } = await getAuthenticatedHttpClient().get(getNotificationsApiUrl(), { params }); - - const startIndex = (page - 1) * pageSize; - const endIndex = startIndex + pageSize; - - const notifications = data.slice(startIndex, endIndex); - return { notifications, numPages: 2, currentPage: page }; +export async function getNotificationsList(appName, page) { + const params = snakeCaseObject({ appName, page }); + const { data } = await getAuthenticatedHttpClient().get(getNotificationsListApiUrl(), { params }); + return data; } export async function getNotificationCounts() { @@ -31,14 +26,14 @@ export async function markNotificationSeen(appName) { export async function markAllNotificationRead(appName) { const params = snakeCaseObject({ appName }); - const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params }); + const { data } = await getAuthenticatedHttpClient().patch(markNotificationAsReadApiUrl(), params); return data; } export async function markNotificationRead(notificationId) { const params = snakeCaseObject({ notificationId }); - const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params }); + 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 index 9c0d82a..a905f6c 100644 --- a/src/Notifications/data/api.test.js +++ b/src/Notifications/data/api.test.js @@ -5,15 +5,15 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { - getNotificationsApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, - getNotificationCounts, getNotifications, markNotificationSeen, markAllNotificationRead, markNotificationRead, + getNotificationsListApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, + getNotificationCounts, getNotificationsList, markNotificationSeen, markAllNotificationRead, markNotificationRead, } from './api'; import './__factories__'; const notificationCountsApiUrl = getNotificationsCountApiUrl(); -const notificationsApiUrl = getNotificationsApiUrl(); -const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions'); +const notificationsApiUrl = getNotificationsListApiUrl(); +const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussion'); const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl(); let axiosMock = null; @@ -43,7 +43,7 @@ describe('Notifications API', () => { expect(count).toEqual(45); expect(countByAppName.reminders).toEqual(10); - expect(countByAppName.discussions).toEqual(20); + expect(countByAppName.discussion).toEqual(20); expect(countByAppName.grades).toEqual(10); expect(countByAppName.authoring).toEqual(5); }); @@ -62,14 +62,11 @@ describe('Notifications API', () => { }); it('Successfully get notifications.', async () => { - axiosMock.onGet(notificationsApiUrl).reply( - 200, - (Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })), - ); + axiosMock.onGet(notificationsApiUrl).reply(200, (Factory.build('notificationsList'))); - const { notifications } = await getNotifications('discussions', 1, 10); + const notifications = await getNotificationsList('discussion', 1); - expect(notifications).toHaveLength(2); + expect(notifications.results).toHaveLength(2); }); it.each([ @@ -78,7 +75,7 @@ describe('Notifications API', () => { ])('%s for notification API.', async ({ statusCode, message }) => { axiosMock.onGet(notificationsApiUrl).reply(statusCode, { message }); try { - await getNotifications({ page: 1, pageSize: 10 }); + await getNotificationsList('discussion', 1); } catch (error) { expect(error.response.status).toEqual(statusCode); expect(error.response.data.message).toEqual(message); @@ -88,7 +85,7 @@ describe('Notifications API', () => { it('Successfully marked all notifications as seen for selected app.', async () => { axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200, { message: 'Notifications marked seen.' }); - const { message } = await markNotificationSeen('discussions'); + const { message } = await markNotificationSeen('discussion'); expect(message).toEqual('Notifications marked seen.'); }); @@ -99,7 +96,7 @@ describe('Notifications API', () => { ])('%s for notification mark as seen API.', async ({ statusCode, message }) => { axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(statusCode, { message }); try { - await markNotificationSeen('discussions'); + await markNotificationSeen('discussion'); } catch (error) { expect(error.response.status).toEqual(statusCode); expect(error.response.data.message).toEqual(message); @@ -107,9 +104,9 @@ describe('Notifications API', () => { }); it('Successfully marked all notifications as read for selected app.', async () => { - axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' }); + axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' }); - const { message } = await markAllNotificationRead('discussions'); + const { message } = await markAllNotificationRead('discussion'); expect(message).toEqual('Notifications marked read.'); }); @@ -118,9 +115,9 @@ describe('Notifications API', () => { { 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.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message }); + axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message }); try { - await markAllNotificationRead('discussions'); + await markAllNotificationRead('discussion'); } catch (error) { expect(error.response.status).toEqual(statusCode); expect(error.response.data.message).toEqual(message); @@ -128,7 +125,7 @@ describe('Notifications API', () => { }); it('Successfully marked notification as read.', async () => { - axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notification marked read.' }); + axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notification marked read.' }); const { data } = await markNotificationRead(1); @@ -139,7 +136,7 @@ describe('Notifications API', () => { { 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.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message }); + axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode, { message }); try { await markAllNotificationRead(1); } catch (error) { diff --git a/src/Notifications/data/redux.test.js b/src/Notifications/data/redux.test.js index fd19943..3ceb0bf 100644 --- a/src/Notifications/data/redux.test.js +++ b/src/Notifications/data/redux.test.js @@ -6,8 +6,9 @@ import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../store'; import executeThunk from '../../test-utils'; +import mockNotificationsResponse from '../test-utils'; import { - getNotificationsApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, + getNotificationsListApiUrl, getNotificationsCountApiUrl, markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, } from './api'; import { fetchAppsNotificationCount, fetchNotificationList, markNotificationsAsRead, markAllNotificationsAsRead, @@ -17,9 +18,9 @@ import { import './__factories__'; const notificationCountsApiUrl = getNotificationsCountApiUrl(); -const notificationsApiUrl = getNotificationsApiUrl(); +const notificationsListApiUrl = getNotificationsListApiUrl(); const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl(); -const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussions'); +const markedAllNotificationsAsSeenApiUrl = markNotificationsSeenApiUrl('discussion'); let axiosMock; let store; @@ -38,13 +39,7 @@ describe('Notification Redux', () => { Factory.resetAll(); store = initializeStore(); - axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); - axiosMock.onGet(notificationsApiUrl).reply( - 200, - (Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })), - ); - await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState); - await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState); + ({ store, axiosMock } = await mockNotificationsResponse()); }); afterEach(() => { @@ -57,30 +52,26 @@ describe('Notification Redux', () => { const { notifications } = store.getState(); expect(notifications.notificationStatus).toEqual('idle'); - expect(notifications.appName).toEqual('discussions'); + 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.count).toEqual(10); - expect(notifications.pagination.numPages).toEqual(1); - expect(notifications.pagination.currentPage).toEqual(1); - expect(notifications.pagination.nextPage).toBeNull(); + expect(notifications.pagination).toEqual({}); }); it('Successfully loaded notifications list in the redux.', async () => { const { notifications: { notifications } } = store.getState(); - - expect(Object.keys(notifications)).toHaveLength(2); + 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(notificationsApiUrl).reply(statusCode); - await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState); + axiosMock.onGet(notificationsListApiUrl).reply(statusCode); + await executeThunk(fetchNotificationList({ page: 1 }), store.dispatch, store.getState); const { notifications: { notificationStatus } } = store.getState(); @@ -92,7 +83,7 @@ describe('Notification Redux', () => { expect(tabsCount.count).toEqual(25); expect(tabsCount.reminders).toEqual(10); - expect(tabsCount.discussions).toEqual(0); + expect(tabsCount.discussion).toEqual(0); expect(tabsCount.grades).toEqual(10); expect(tabsCount.authoring).toEqual(5); }); @@ -111,7 +102,7 @@ describe('Notification Redux', () => { it('Successfully marked all notifications as seen for selected app.', async () => { axiosMock.onPut(markedAllNotificationsAsSeenApiUrl).reply(200); - await executeThunk(markNotificationsAsSeen('discussions'), store.dispatch, store.getState); + await executeThunk(markNotificationsAsSeen('discussion'), store.dispatch, store.getState); expect(store.getState().notifications.notificationStatus).toEqual('successful'); }); @@ -121,7 +112,7 @@ describe('Notification Redux', () => { { 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('discussions'), store.dispatch, store.getState); + await executeThunk(markNotificationsAsSeen('discussion'), store.dispatch, store.getState); const { notifications: { notificationStatus } } = store.getState(); @@ -129,8 +120,8 @@ describe('Notification Redux', () => { }); it('Successfully marked all notifications as read for selected app in the redux.', async () => { - axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200); - await executeThunk(markAllNotificationsAsRead('discussions'), store.dispatch, store.getState); + 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]; @@ -140,7 +131,7 @@ describe('Notification Redux', () => { }); it('Successfully marked notification as read in the redux.', async () => { - axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200); + axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200); await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState); const { notifications: { notificationStatus, notifications } } = store.getState(); @@ -154,7 +145,7 @@ describe('Notification Redux', () => { { statusCode: 404, status: 'failed' }, { statusCode: 403, status: 'denied' }, ])('%s to marked notification as read in the redux.', async ({ statusCode, status }) => { - axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(statusCode); + axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(statusCode); await executeThunk(markNotificationsAsRead(1), store.dispatch, store.getState); const { notifications: { notificationStatus } } = store.getState(); diff --git a/src/Notifications/data/selector.test.jsx b/src/Notifications/data/selector.test.jsx index bc73a3f..31f2c80 100644 --- a/src/Notifications/data/selector.test.jsx +++ b/src/Notifications/data/selector.test.jsx @@ -5,8 +5,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeStore } from '../../store'; -import executeThunk from '../../test-utils'; -import { getNotificationsApiUrl, getNotificationsCountApiUrl } from './api'; +import mockNotificationsResponse from '../test-utils'; import { selectNotifications, selectNotificationsByIds, @@ -18,13 +17,9 @@ import { selectSelectedAppNotificationIds, selectShowNotificationTray, } from './selectors'; -import { fetchAppsNotificationCount, fetchNotificationList } from './thunks'; import './__factories__'; -const notificationCountsApiUrl = getNotificationsCountApiUrl(); -const notificationsApiUrl = getNotificationsApiUrl(); - let axiosMock; let store; @@ -42,13 +37,7 @@ describe('Notification Selectors', () => { Factory.resetAll(); store = initializeStore(); - axiosMock.onGet(notificationCountsApiUrl).reply(200, (Factory.build('notificationsCount'))); - axiosMock.onGet(notificationsApiUrl).reply( - 200, - (Factory.buildList('notification', 2, null, { createdDate: new Date().toISOString() })), - ); - await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState); - await executeThunk(fetchNotificationList({ page: 1, pageSize: 10 }), store.dispatch, store.getState); + ({ store, axiosMock } = await mockNotificationsResponse()); }); afterEach(() => { @@ -68,7 +57,7 @@ describe('Notification Selectors', () => { expect(tabsCount.count).toEqual(25); expect(tabsCount.reminders).toEqual(10); - expect(tabsCount.discussions).toEqual(0); + expect(tabsCount.discussion).toEqual(0); expect(tabsCount.grades).toEqual(10); expect(tabsCount.authoring).toEqual(5); }); @@ -82,9 +71,9 @@ describe('Notification Selectors', () => { it('Should return selected app notification ids.', async () => { const state = store.getState(); - const notificationIds = selectSelectedAppNotificationIds('discussions')(state); + const notificationIds = selectSelectedAppNotificationIds('discussion')(state); - expect(notificationIds).toHaveLength(2); + expect(notificationIds).toHaveLength(10); }); it('Should return show notification tray status.', async () => { @@ -98,29 +87,29 @@ describe('Notification Selectors', () => { const state = store.getState(); const notifications = selectNotifications()(state); - expect(Object.keys(notifications)).toHaveLength(2); + expect(Object.keys(notifications)).toHaveLength(10); }); it('Should return notifications from Ids.', async () => { const state = store.getState(); - const notifications = selectNotificationsByIds('discussions')(state); + const notifications = selectNotificationsByIds('discussion')(state); - expect(notifications).toHaveLength(2); + expect(notifications).toHaveLength(10); }); it('Should return selected app name.', async () => { const state = store.getState(); const appName = selectSelectedAppName()(state); - expect(appName).toEqual('discussions'); + expect(appName).toEqual('discussion'); }); it('Should return pagination data.', async () => { const state = store.getState(); const paginationData = selectPaginationData()(state); - expect(paginationData.count).toEqual(10); expect(paginationData.currentPage).toEqual(1); expect(paginationData.numPages).toEqual(2); + expect(paginationData.hasMorePages).toEqual(true); }); }); diff --git a/src/Notifications/data/slice.js b/src/Notifications/data/slice.js index 412cc4d..6ee4178 100644 --- a/src/Notifications/data/slice.js +++ b/src/Notifications/data/slice.js @@ -3,26 +3,21 @@ import { createSlice } from '@reduxjs/toolkit'; export const RequestStatus = { IDLE: 'idle', - LOADING: 'in-progress', - LOADED: 'successful', + IN_PROGRESS: 'in-progress', + SUCCESSFUL: 'successful', FAILED: 'failed', DENIED: 'denied', }; const initialState = { - notificationStatus: 'idle', - appName: 'discussions', + notificationStatus: RequestStatus.IDLE, + appName: 'discussion', appsId: [], apps: {}, notifications: {}, tabsCount: {}, showNotificationsTray: false, - pagination: { - count: 10, - numPages: 1, - currentPage: 1, - nextPage: null, - }, + pagination: {}, }; const slice = createSlice({ name: 'notifications', @@ -35,21 +30,19 @@ const slice = createSlice({ state.notificationStatus = RequestStatus.FAILED; }, fetchNotificationRequest: (state) => { - state.notificationStatus = RequestStatus.LOADING; + state.notificationStatus = RequestStatus.IN_PROGRESS; }, fetchNotificationSuccess: (state, { payload }) => { const { - newNotificationIds, notificationsKeyValuePair, numPages, currentPage, + 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.LOADED; - state.pagination.numPages = numPages; - state.pagination.currentPage = currentPage; + state.notificationStatus = RequestStatus.SUCCESSFUL; + state.pagination = pagination; }, fetchNotificationsCountDenied: (state) => { state.notificationStatus = RequestStatus.DENIED; @@ -58,7 +51,7 @@ const slice = createSlice({ state.notificationStatus = RequestStatus.FAILED; }, fetchNotificationsCountRequest: (state) => { - state.notificationStatus = RequestStatus.LOADING; + state.notificationStatus = RequestStatus.IN_PROGRESS; }, fetchNotificationsCountSuccess: (state, { payload }) => { const { @@ -68,13 +61,13 @@ const slice = createSlice({ state.appsId = appIds; state.apps = apps; state.showNotificationsTray = showNotificationsTray; - state.notificationStatus = RequestStatus.LOADED; + state.notificationStatus = RequestStatus.SUCCESSFUL; }, markNotificationsAsSeenRequest: (state) => { - state.notificationStatus = RequestStatus.LOADING; + state.notificationStatus = RequestStatus.IN_PROGRESS; }, markNotificationsAsSeenSuccess: (state) => { - state.notificationStatus = RequestStatus.LOADED; + state.notificationStatus = RequestStatus.SUCCESSFUL; }, markNotificationsAsSeenDenied: (state) => { state.notificationStatus = RequestStatus.DENIED; @@ -83,7 +76,7 @@ const slice = createSlice({ state.notificationStatus = RequestStatus.FAILED; }, markAllNotificationsAsReadRequest: (state) => { - state.notificationStatus = RequestStatus.LOADING; + state.notificationStatus = RequestStatus.IN_PROGRESS; }, markAllNotificationsAsReadSuccess: (state) => { const updatedNotifications = Object.fromEntries( @@ -92,7 +85,7 @@ const slice = createSlice({ ]), ); state.notifications = updatedNotifications; - state.notificationStatus = RequestStatus.LOADED; + state.notificationStatus = RequestStatus.SUCCESSFUL; }, markAllNotificationsAsReadDenied: (state) => { state.notificationStatus = RequestStatus.DENIED; @@ -101,12 +94,12 @@ const slice = createSlice({ state.notificationStatus = RequestStatus.FAILED; }, markNotificationsAsReadRequest: (state) => { - state.notificationStatus = RequestStatus.LOADING; + 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.LOADED; + state.notificationStatus = RequestStatus.SUCCESSFUL; }, markNotificationsAsReadDenied: (state) => { state.notificationStatus = RequestStatus.DENIED; diff --git a/src/Notifications/data/thunks.js b/src/Notifications/data/thunks.js index f87e2e1..585ce0e 100644 --- a/src/Notifications/data/thunks.js +++ b/src/Notifications/data/thunks.js @@ -23,7 +23,7 @@ import { markNotificationsAsReadFailure, } from './slice'; import { - getNotifications, getNotificationCounts, markNotificationSeen, markAllNotificationRead, markNotificationRead, + getNotificationsList, getNotificationCounts, markNotificationSeen, markAllNotificationRead, markNotificationRead, } from './api'; import { getHttpErrorStatus } from '../utils'; @@ -35,21 +35,26 @@ const normalizeNotificationCounts = ({ countByAppName, count, showNotificationsT }; }; -const normalizeNotifications = ({ notifications }) => { - const newNotificationIds = notifications.map(notification => notification.id.toString()); - const notificationsKeyValuePair = notifications.reduce((acc, obj) => { acc[obj.id] = obj; return acc; }, {}); +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, + newNotificationIds, notificationsKeyValuePair, pagination, }; }; -export const fetchNotificationList = ({ appName, page, pageSize }) => ( +export const fetchNotificationList = ({ appName, page }) => ( async (dispatch) => { try { dispatch(fetchNotificationRequest({ appName })); - const data = await getNotifications(appName, page, pageSize); + const data = await getNotificationsList(appName, page); const normalisedData = normalizeNotifications((camelCaseObject(data))); - dispatch(fetchNotificationSuccess({ ...normalisedData, numPages: data.numPages, currentPage: data.currentPage })); + dispatch(fetchNotificationSuccess({ ...normalisedData })); } catch (error) { if (getHttpErrorStatus(error) === 403) { dispatch(fetchNotificationDenied(appName)); diff --git a/src/Notifications/notificationRowItem.test.jsx b/src/Notifications/notificationRowItem.test.jsx index 5443ab4..332a026 100644 --- a/src/Notifications/notificationRowItem.test.jsx +++ b/src/Notifications/notificationRowItem.test.jsx @@ -73,7 +73,7 @@ describe('Notification row item test cases.', () => { ); it('Successfully marked notification as read.', async () => { - axiosMock.onPut(markedNotificationAsReadApiUrl).reply(200, { message: 'Notification marked read.' }); + axiosMock.onPatch(markedNotificationAsReadApiUrl).reply(200, { message: 'Notification marked read.' }); renderComponent(); const bellIcon = screen.queryByTestId('notification-bell-icon'); diff --git a/src/Notifications/notificationSections.test.jsx b/src/Notifications/notificationSections.test.jsx index 2130325..1495fac 100644 --- a/src/Notifications/notificationSections.test.jsx +++ b/src/Notifications/notificationSections.test.jsx @@ -14,9 +14,10 @@ import { AppContext, AppProvider } from '@edx/frontend-platform/react'; import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown'; import { initializeStore } from '../store'; -import { markNotificationAsReadApiUrl } from './data/api'; +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(); @@ -69,7 +70,7 @@ describe('Notification sections test cases.', () => { }); it('Successfully marked all notifications as read, removing the unread status.', async () => { - axiosMock.onPut(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' }); + axiosMock.onPatch(markedAllNotificationsAsReadApiUrl).reply(200, { message: 'Notifications marked read.' }); renderComponent(); const bellIcon = screen.queryByTestId('notification-bell-icon'); @@ -83,16 +84,23 @@ describe('Notification sections test cases.', () => { }); 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'); - expect(screen.queryAllByTestId('notification-contents')).toHaveLength(10); - await act(async () => { fireEvent.click(loadMoreButton); }); + 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); - expect(screen.queryAllByTestId('notification-contents')).toHaveLength(16); + 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 index 8bec8b8..50e1e63 100644 --- a/src/Notifications/notificationTabs.test.jsx +++ b/src/Notifications/notificationTabs.test.jsx @@ -59,7 +59,7 @@ describe('Notification Tabs test cases.', () => { const selectedTab = tabs.find(tab => tab.getAttribute('aria-selected') === 'true'); expect(tabs.length).toEqual(5); - expect(within(selectedTab).queryByText('discussions')).toBeInTheDocument(); + expect(within(selectedTab).queryByText('discussion')).toBeInTheDocument(); expect(within(selectedTab).queryByRole('status')).not.toBeInTheDocument(); }); diff --git a/src/Notifications/test-utils.js b/src/Notifications/test-utils.js index 85489ce..c0dcd58 100644 --- a/src/Notifications/test-utils.js +++ b/src/Notifications/test-utils.js @@ -5,27 +5,28 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { initializeStore } from '../store'; import executeThunk from '../test-utils'; -import { getNotificationsApiUrl, getNotificationsCountApiUrl } from './data/api'; +import { getNotificationsListApiUrl, getNotificationsCountApiUrl } from './data/api'; import { fetchAppsNotificationCount, fetchNotificationList } from './data/thunks'; import './data/__factories__'; const notificationCountsApiUrl = getNotificationsCountApiUrl(); -const notificationsApiUrl = getNotificationsApiUrl(); +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.buildList('notification', 8, null, { createdDate: new Date().toISOString() }).concat( - Factory.buildList('notification', 8, null, { createdDate: '2023-06-01T00:46:11.979531Z' }), - )), - ); + 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({ page: 1, pageSize: 10 }), store.dispatch, store.getState); + await executeThunk(fetchNotificationList({ appName: 'discussion', page: 1 }), store.dispatch, store.getState); return { store, axiosMock }; } diff --git a/src/index.scss b/src/index.scss index 1e1eeca..46ed12d 100644 --- a/src/index.scss +++ b/src/index.scss @@ -123,7 +123,7 @@ $white: #fff; } .content { - b { + strong { color: #00262B !important; font-weight: 500 !important; } diff --git a/src/learning-header/AuthenticatedUserDropdown.jsx b/src/learning-header/AuthenticatedUserDropdown.jsx index 0060c50..889ff3f 100644 --- a/src/learning-header/AuthenticatedUserDropdown.jsx +++ b/src/learning-header/AuthenticatedUserDropdown.jsx @@ -24,7 +24,7 @@ const AuthenticatedUserDropdown = ({ intl, username }) => { dispatch(fetchAppsNotificationCount()); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [notificationStatus]); + }, []); const dashboardMenuItem = (