From a3e454728569826334f8a15e633926d98efce1e0 Mon Sep 17 00:00:00 2001 From: Cyril Nxumalo Date: Fri, 13 Sep 2024 15:06:31 +0200 Subject: [PATCH] chore: Migrate deprecated Table to DataTable for EnrolledLearnersForInactiveCoursesTable --- ...edLearnersForInactiveCoursesTable.test.jsx | 174 ++-- ...rnersForInactiveCoursesTable.test.jsx.snap | 815 +++++++++++++----- .../useEnrolledLearnersForInactiveCourses.js | 92 ++ .../index.jsx | 124 +-- src/eventTracking.js | 6 + 5 files changed, 845 insertions(+), 366 deletions(-) create mode 100644 src/components/EnrolledLearnersForInactiveCoursesTable/data/hooks/useEnrolledLearnersForInactiveCourses.js diff --git a/src/components/EnrolledLearnersForInactiveCoursesTable/EnrolledLearnersForInactiveCoursesTable.test.jsx b/src/components/EnrolledLearnersForInactiveCoursesTable/EnrolledLearnersForInactiveCoursesTable.test.jsx index 5274ad4087..0170859941 100644 --- a/src/components/EnrolledLearnersForInactiveCoursesTable/EnrolledLearnersForInactiveCoursesTable.test.jsx +++ b/src/components/EnrolledLearnersForInactiveCoursesTable/EnrolledLearnersForInactiveCoursesTable.test.jsx @@ -8,107 +8,78 @@ import { Provider } from 'react-redux'; import { mount } from 'enzyme'; import EnrolledLearnersForInactiveCoursesTable from '.'; +import useEnrolledLearnersForInactiveCourses from './data/hooks/useEnrolledLearnersForInactiveCourses'; const enterpriseId = 'test-enterprise'; const mockStore = configureMockStore([thunk]); -const enrolledLearnersForInactiveCoursesEmptyStore = mockStore({ + +jest.mock('./data/hooks/useEnrolledLearnersForInactiveCourses', () => ( + jest.fn().mockReturnValue({}) +)); + +const store = mockStore({ portalConfiguration: { enterpriseId, }, - table: { - 'enrolled-learners-inactive-courses': { - data: { - results: [], - current_page: 1, - num_pages: 1, - }, - ordering: null, - loading: false, - error: null, - }, - }, }); -const enrolledLearnersForInactiveCoursesStore = mockStore({ - portalConfiguration: { - enterpriseId, - }, - table: { - 'enrolled-learners-inactive-courses': { - data: { - count: 3, - num_pages: 1, - current_page: 1, - results: [ - { - id: 1, - enterprise_id: '72416e52-8c77-4860-9584-15e5b06220fb', - lms_user_id: 11, - enterprise_user_id: 222, - enterprise_sso_uid: 'harry', - user_account_creation_timestamp: '2015-02-12T23:14:35Z', - user_email: 'test_user_1@example.com', - user_username: 'test_user_1', - user_country_code: 'US', - last_activity_date: '2017-06-23', - enrollment_count: 2, - course_completion_count: 1, - }, - { - id: 1, - enterprise_id: '72416e52-8c77-4860-9584-15e5b06220fb', - lms_user_id: 22, - enterprise_user_id: 333, - enterprise_sso_uid: 'harry', - user_account_creation_timestamp: '2016-05-12T22:14:36Z', - user_email: 'test_user_2@example.com', - user_username: 'test_user_2', - user_country_code: 'US', - last_activity_date: '2018-01-15', - enrollment_count: 5, - course_completion_count: 5, - }, - { - id: 1, - enterprise_id: '72416e52-8c77-4860-9584-15e5b06220fb', - lms_user_id: 33, - enterprise_user_id: 444, - enterprise_sso_uid: 'harry', - user_account_creation_timestamp: '2017-12-12T18:10:15Z', - user_email: 'test_user_3@example.com', - user_username: 'test_user_3', - user_country_code: 'US', - last_activity_date: '2017-11-18', - enrollment_count: 6, - course_completion_count: 4, - }, - ], - next: null, - start: 0, - previous: null, + +const mockUseEnrolledLearnersForInactiveCourses = { + isLoading: false, + enrolledLearnersForInactiveCourses: { + itemCount: 3, + pageCount: 1, + results: [ + { + id: 1, + enterpriseId: '72416e52-8c77-4860-9584-15e5b06220fb', + lmsUserId: 11, + enterpriseUserId: 222, + enterpriseSsoUid: 'harry', + userAccountCreationTimestamp: '2015-02-12T23:14:35Z', + userEmail: 'test_user_1@example.com', + userUsername: 'test_user_1', + userCountryCode: 'US', + lastActivityDate: '2017-06-23', + enrollmentCount: 2, + courseCompletionCount: 1, + }, + { + id: 1, + enterpriseId: '72416e52-8c77-4860-9584-15e5b06220fb', + lmsUserId: 22, + enterpriseUserId: 333, + enterpriseSsoUid: 'harry', + userAccountCreationTimestamp: '2016-05-12T22:14:36Z', + userEmail: 'test_user_2@example.com', + userUsername: 'test_user_2', + userCountryCode: 'US', + lastActivityDate: '2018-01-15', + enrollmentCount: 5, + courseCompletionCount: 5, + }, + { + id: 1, + enterpriseId: '72416e52-8c77-4860-9584-15e5b06220fb', + lmsUserId: 33, + enterpriseUserId: 444, + enterpriseSsoUid: 'harry', + userAccountCreationTimestamp: '2017-12-12T18:10:15Z', + userEmail: 'test_user_3@example.com', + userUsername: 'test_user_3', + userCountryCode: 'US', + lastActivityDate: '2017-11-18', + enrollmentCount: 6, + courseCompletionCount: 4, }, - ordering: null, - loading: false, - error: null, - }, + ], }, -}); - -const EnrolledLearnersForInactiveCoursesEmptyTableWrapper = props => ( - - - - - - - -); + fetchEnrolledLearnersForInactiveCourses: jest.fn(), +}; const EnrolledLearnersForInactiveCoursesWrapper = props => ( - + @@ -119,15 +90,27 @@ const EnrolledLearnersForInactiveCoursesWrapper = props => ( describe('EnrolledLearnersForInactiveCoursesTable', () => { it('renders empty state correctly', () => { + useEnrolledLearnersForInactiveCourses.mockReturnValueOnce({ + ...mockUseEnrolledLearnersForInactiveCourses, + enrolledLearnersForInactiveCourses: { + itemCount: 0, + pageCount: 0, + results: [], + }, + }); const tree = renderer .create(( - + )) .toJSON(); expect(tree).toMatchSnapshot(); }); it('renders enrolled learners for inactive courses table correctly', () => { + useEnrolledLearnersForInactiveCourses.mockReturnValueOnce( + mockUseEnrolledLearnersForInactiveCourses, + ); + const tree = renderer .create(( @@ -137,7 +120,10 @@ describe('EnrolledLearnersForInactiveCoursesTable', () => { }); it('renders enrolled learners for inactive courses table with correct data', () => { - const tableId = 'enrolled-learners-inactive-courses'; + useEnrolledLearnersForInactiveCourses.mockReturnValueOnce( + mockUseEnrolledLearnersForInactiveCourses, + ); + const columnTitles = [ 'Email', 'Total Course Enrollment Count', 'Total Completed Courses Count', 'Last Activity Date', ]; @@ -167,18 +153,18 @@ describe('EnrolledLearnersForInactiveCoursesTable', () => { )); // Verify that table has correct number of columns - expect(wrapper.find(`.${tableId} thead th`).length).toEqual(columnTitles.length); + expect(wrapper.find('[role="table"] thead th').length).toEqual(columnTitles.length); // Verify only expected columns are shown - wrapper.find(`.${tableId} thead th`).forEach((column, index) => { + wrapper.find('[role="table"] thead th').forEach((column, index) => { expect(column.text()).toContain(columnTitles[index]); }); // Verify that table has correct number of rows - expect(wrapper.find(`.${tableId} tbody tr`).length).toEqual(rowsData.length); + expect(wrapper.find('[role="table"] tbody tr').length).toEqual(rowsData.length); // Verify each row in table has correct data - wrapper.find(`.${tableId} tbody tr`).forEach((row, rowIndex) => { + wrapper.find('[role="table"] tbody tr').forEach((row, rowIndex) => { row.find('td').forEach((cell, colIndex) => { expect(cell.text()).toEqual(rowsData[rowIndex][colIndex]); }); diff --git a/src/components/EnrolledLearnersForInactiveCoursesTable/__snapshots__/EnrolledLearnersForInactiveCoursesTable.test.jsx.snap b/src/components/EnrolledLearnersForInactiveCoursesTable/__snapshots__/EnrolledLearnersForInactiveCoursesTable.test.jsx.snap index 9b25c637a2..0b79d62300 100644 --- a/src/components/EnrolledLearnersForInactiveCoursesTable/__snapshots__/EnrolledLearnersForInactiveCoursesTable.test.jsx.snap +++ b/src/components/EnrolledLearnersForInactiveCoursesTable/__snapshots__/EnrolledLearnersForInactiveCoursesTable.test.jsx.snap @@ -2,35 +2,322 @@ exports[`EnrolledLearnersForInactiveCoursesTable renders empty state correctly 1`] = `
- - - - -
- There are no results. +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + +
+ + + Email + + + + + + + + + + + Total Course Enrollment Count + + + + + + + + + + + Total Completed Courses Count + + + + + + + + + + + Last Activity Date + + + + + + + +
+
+
+ No results found +
+
+ +
@@ -38,145 +325,236 @@ exports[`EnrolledLearnersForInactiveCoursesTable renders empty state correctly 1 exports[`EnrolledLearnersForInactiveCoursesTable renders enrolled learners for inactive courses table correctly 1`] = `
- - +
+ Showing 1 - 3 of 3. +
+ +
+
+
+
+
+
+
+
+ @@ -270,101 +650,104 @@ exports[`EnrolledLearnersForInactiveCoursesTable renders enrolled learners for i
- + - + - + - +
2 1 June 23, 2017
5 5 January 15, 2018
6 4 November 18, 2017
-
-
-
-
-
- - -
  • -
  • +
  • -
    - Next +
    - -
  • - - + + + + +
    diff --git a/src/components/EnrolledLearnersForInactiveCoursesTable/data/hooks/useEnrolledLearnersForInactiveCourses.js b/src/components/EnrolledLearnersForInactiveCoursesTable/data/hooks/useEnrolledLearnersForInactiveCourses.js new file mode 100644 index 0000000000..7fe056ef08 --- /dev/null +++ b/src/components/EnrolledLearnersForInactiveCoursesTable/data/hooks/useEnrolledLearnersForInactiveCourses.js @@ -0,0 +1,92 @@ +import { + useCallback, useMemo, useRef, useState, +} from 'react'; +import { camelCaseObject } from '@edx/frontend-platform/utils'; +import debounce from 'lodash.debounce'; +import { logError } from '@edx/frontend-platform/logging'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; +import EVENT_NAMES from '../../../../eventTracking'; + +const applySortByToOptions = (sortBy, options) => { + if (!sortBy || sortBy.length === 0) { + return; + } + const apiFieldsForColumnAccessor = { + userEmail: { key: 'user_email' }, + enrollmentCount: { key: 'enrollment_count' }, + courseCompletionCount: { key: 'course_completion_count' }, + lastActivityDate: { key: 'last_activity_date' }, + }; + const orderingStrings = sortBy.map(({ id, desc }) => { + const apiFieldForColumnAccessor = apiFieldsForColumnAccessor[id]; + if (!apiFieldForColumnAccessor) { + return undefined; + } + const apiFieldKey = apiFieldForColumnAccessor.key; + return desc ? `-${apiFieldKey}` : apiFieldKey; + }).filter(orderingString => !!orderingString); + + Object.assign(options, { + ordering: orderingStrings.join(','), + }); +}; + +const useEnrolledLearnersForInactiveCourses = (enterpriseId) => { + const shouldTrackFetchEvents = useRef(false); + const [isLoading, setIsLoading] = useState(true); + const [enrolledLearnersForInactiveCourses, setEnrolledLearnersForInactiveCourses] = useState({ + itemCount: 0, + pageCount: 0, + results: [], + }); + + const fetchEnrolledLearnersForInactiveCourses = useCallback(async (args) => { + try { + setIsLoading(true); + const options = { + page: args.pageIndex + 1, + pageSize: args.pageSize, + }; + applySortByToOptions(args.sortBy, options); + + const response = await EnterpriseDataApiService.fetchEnrolledLearnersForInactiveCourses(enterpriseId, options); + const data = camelCaseObject(response.data); + setEnrolledLearnersForInactiveCourses({ + itemCount: data.count, + pageCount: data.numPages ?? Math.floor(data.count / options.pageSize), + results: data.results, + }); + + if (shouldTrackFetchEvents.current) { + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.PROGRESS_REPORT.DATATABLE_SORT_BY_OR_FILTER, + { + tableId: '', + ...options, + }, + ); + } else { + shouldTrackFetchEvents.current = true; + } + } catch (error) { + logError(error); + } finally { + setIsLoading(false); + } + }, [enterpriseId]); + + const debouncedFetchEnrolledLearnersForInactiveCourses = useMemo( + () => debounce(fetchEnrolledLearnersForInactiveCourses, 300), + [fetchEnrolledLearnersForInactiveCourses], + ); + + return { + isLoading, + enrolledLearnersForInactiveCourses, + fetchEnrolledLearnersForInactiveCourses: debouncedFetchEnrolledLearnersForInactiveCourses, + }; +}; + +export default useEnrolledLearnersForInactiveCourses; diff --git a/src/components/EnrolledLearnersForInactiveCoursesTable/index.jsx b/src/components/EnrolledLearnersForInactiveCoursesTable/index.jsx index 9a8145e43d..fab2b52362 100644 --- a/src/components/EnrolledLearnersForInactiveCoursesTable/index.jsx +++ b/src/components/EnrolledLearnersForInactiveCoursesTable/index.jsx @@ -1,69 +1,81 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; -import TableContainer from '../../containers/TableContainer'; +import { DataTable } from '@openedx/paragon'; import { i18nFormatTimestamp } from '../../utils'; -import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; +import useEnrolledLearnersForInactiveCourses from './data/hooks/useEnrolledLearnersForInactiveCourses'; -const EnrolledLearnersForInactiveCoursesTable = () => { - const intl = useIntl(); +const UserEmail = ({ row }) => ( + {row.original.userEmail} +); - const tableColumns = [ - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.enrolled.learners.inactive.courses.table.user_email.column.heading', - defaultMessage: 'Email', - description: 'Column heading for the user email column in the enrolled learners table for inactive courses', - }), - key: 'user_email', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.enrolled.learners.inactive.courses.table.enrollment_count.column.heading', - defaultMessage: 'Total Course Enrollment Count', - description: 'Column heading for the course enrollment count column in the enrolled learners table for inactive courses', - }), - key: 'enrollment_count', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.enrolled.learners.inactive.courses.table.course_completion_count.column.heading', - defaultMessage: 'Total Completed Courses Count', - description: 'Column heading for the completed courses count column in the enrolled learners table for inactive courses', - }), - key: 'course_completion_count', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.enrolled.learners.inactive.courses.table.last_activity_date.column.heading', - defaultMessage: 'Last Activity Date', - description: 'Column heading for the last activity date column in the enrolled learners table for inactive courses', - }), - key: 'last_activity_date', - columnSortable: true, - }, - ]; +UserEmail.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + userEmail: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; - const formatLearnerData = learners => learners.map(learner => ({ - ...learner, - user_email: {learner.user_email}, - last_activity_date: i18nFormatTimestamp({ - intl, timestamp: learner.last_activity_date, - }), - })); +const EnrolledLearnersForInactiveCoursesTable = (enterpriseId) => { + const intl = useIntl(); + const { + isLoading, + enrolledLearnersForInactiveCourses: tableData, + fetchEnrolledLearnersForInactiveCourses: fetchTableData, + } = useEnrolledLearnersForInactiveCourses(enterpriseId); return ( - i18nFormatTimestamp({ intl, timestamp: value }), + }, + ]} + initialState={{ + pageIndex: 20, + pageSize: 0, + sortBy: [{ id: 'lastActivityDate', desc: true }], + selectedRowsOrdered: [], + }} + fetchData={fetchTableData} + data={tableData.results} + itemCount={tableData.itemCount} + pageCount={tableData.pageCount} /> ); }; diff --git a/src/eventTracking.js b/src/eventTracking.js index f7dfd9872f..7525d89a36 100644 --- a/src/eventTracking.js +++ b/src/eventTracking.js @@ -17,6 +17,7 @@ const SUBSCRIPTION_PREFIX = `${PROJECT_NAME}.subscriptions`; const SETTINGS_PREFIX = `${PROJECT_NAME}.settings`; const CONTENT_HIGHLIGHTS_PREFIX = `${PROJECT_NAME}.content_highlights`; const LEARNER_CREDIT_MANAGEMENT_PREFIX = `${PROJECT_NAME}.learner_credit_management`; +const PROGRESS_REPORT_PREFIX = `${PROJECT_NAME}.progress_report`; // Sub-prefixes // Subscriptions @@ -95,6 +96,10 @@ export const CONTENT_HIGHLIGHTS_EVENTS = { const SETTINGS_ACCESS_PREFIX = `${SETTINGS_PREFIX}.ACCESS`; +export const PROGRESS_REPORT_EVENTS = { + DATATABLE_SORT_BY_OR_FILTER: `${PROGRESS_REPORT_PREFIX}.datatable.sort_by_or_filter.changed`, +}; + export const SETTINGS_ACCESS_EVENTS = { UNIVERSAL_LINK_TOGGLE: `${SETTINGS_ACCESS_PREFIX}.universal-link.toggle.clicked`, UNIVERSAL_LINK_GENERATE: `${SETTINGS_ACCESS_PREFIX}.universal-link.generate.clicked`, @@ -184,6 +189,7 @@ const EVENT_NAMES = { SUBSCRIPTIONS: SUBSCRIPTION_EVENTS, CONTENT_HIGHLIGHTS: CONTENT_HIGHLIGHTS_EVENTS, LEARNER_CREDIT_MANAGEMENT: LEARNER_CREDIT_MANAGEMENT_EVENTS, + PROGRESS_REPORT: PROGRESS_REPORT_EVENTS, }; export default EVENT_NAMES;