From 51acf3563c06df9a9a867596bb579dd4121aae6f Mon Sep 17 00:00:00 2001 From: Cyril Nxumalo Date: Fri, 30 Aug 2024 11:46:22 +0200 Subject: [PATCH] chore: Migrate deprecated Table to DataTable for PastWeekPassedLearnersTable --- .../PastWeekPassedLearnersTable.test.jsx | 75 ++-- .../PastWeekPassedLearnersTable.test.jsx.snap | 399 +++++++++++------- .../data/hooks/usePastWeekPassedLearners.js | 95 +++++ .../PastWeekPassedLearnersTable/index.jsx | 124 +++--- src/eventTracking.js | 6 + 5 files changed, 450 insertions(+), 249 deletions(-) create mode 100644 src/components/PastWeekPassedLearnersTable/data/hooks/usePastWeekPassedLearners.js diff --git a/src/components/PastWeekPassedLearnersTable/PastWeekPassedLearnersTable.test.jsx b/src/components/PastWeekPassedLearnersTable/PastWeekPassedLearnersTable.test.jsx index fab3445cd9..0f53a95ba5 100644 --- a/src/components/PastWeekPassedLearnersTable/PastWeekPassedLearnersTable.test.jsx +++ b/src/components/PastWeekPassedLearnersTable/PastWeekPassedLearnersTable.test.jsx @@ -8,6 +8,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import { mount } from 'enzyme'; import PastWeekPassedLearnersTable from '.'; +import usePastWeekPassedLearners from './data/hooks/usePastWeekPassedLearners'; const enterpriseId = 'test-enterprise'; const mockStore = configureMockStore([thunk]); @@ -15,39 +16,36 @@ const store = mockStore({ portalConfiguration: { enterpriseId, }, - table: { - 'completed-learners-week': { - data: { - count: 2, - num_pages: 1, - current_page: 1, - results: [ - { - id: 1, - passed_date: '2018-09-23T16:27:34.690065Z', - course_title: 'Dive into ReactJS', - course_key: 'edX/ReactJS', - user_email: 'awesome.me@example.com', - }, - { - id: 5, - passed_date: '2018-09-22T16:27:34.690065Z', - course_title: 'Redux with ReactJS', - course_key: 'edX/Redux_ReactJS', - user_email: 'new@example.com', +}); - }, - ], - next: null, - start: 0, - previous: null, +const mockUsePastWeekPassedLearners = { + isLoading: false, + pastWeekPassedLearners: { + itemCount: 2, + pageCount: 1, + results: [ + { + id: 1, + passedDate: '2018-09-23T16:27:34.690065Z', + courseTitle: 'Dive into ReactJS', + courseKey: 'edX/ReactJS', + userEmail: 'awesome.me@example.com', + }, + { + id: 5, + passedDate: '2018-09-22T16:27:34.690065Z', + courseTitle: 'Redux with ReactJS', + courseKey: 'edX/Redux_ReactJS', + userEmail: 'new@example.com', }, - ordering: null, - loading: false, - error: null, - }, + ], }, -}); + fetchPastWeekPassedLearners: jest.fn(), +}; + +jest.mock('./data/hooks/usePastWeekPassedLearners', () => ( + jest.fn().mockReturnValue({}) +)); const PastWeekPassedLearnersWrapper = props => ( @@ -62,7 +60,11 @@ const PastWeekPassedLearnersWrapper = props => ( ); describe('PastWeekPassedLearnersTable', () => { - let wrapper; + beforeEach(() => { + usePastWeekPassedLearners.mockReturnValue(mockUsePastWeekPassedLearners); + }); + + afterEach(() => jest.clearAllMocks()); it('renders table correctly', () => { const tree = renderer @@ -74,7 +76,6 @@ describe('PastWeekPassedLearnersTable', () => { }); it('renders table with correct data', () => { - const tableId = 'completed-learners-week'; const columnTitles = ['Email', 'Course Title', 'Passed Date']; const rowsData = [ [ @@ -89,23 +90,23 @@ describe('PastWeekPassedLearnersTable', () => { ], ]; - wrapper = mount(( + const wrapper = mount(( )); // Verify that table has correct number of columns - expect(wrapper.find(`.${tableId} thead th`).length).toEqual(3); + expect(wrapper.find('[role="table"] thead th').length).toEqual(3); // 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(2); + expect(wrapper.find('[role="table"] tbody tr').length).toEqual(2); // 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/PastWeekPassedLearnersTable/__snapshots__/PastWeekPassedLearnersTable.test.jsx.snap b/src/components/PastWeekPassedLearnersTable/__snapshots__/PastWeekPassedLearnersTable.test.jsx.snap index e09c4f49a6..a15c956709 100644 --- a/src/components/PastWeekPassedLearnersTable/__snapshots__/PastWeekPassedLearnersTable.test.jsx.snap +++ b/src/components/PastWeekPassedLearnersTable/__snapshots__/PastWeekPassedLearnersTable.test.jsx.snap @@ -2,118 +2,192 @@ exports[`PastWeekPassedLearnersTable renders table correctly 1`] = `
- - +
+ Showing 1 - 2 of 2. +
+ +
+
+
+
+
+
+
+
+ @@ -163,101 +238,105 @@ exports[`PastWeekPassedLearnersTable renders table correctly 1`] = `
- + - + - +
Dive into ReactJS September 23, 2018
Redux with ReactJS September 22, 2018
-
-
-
-
-
- - -
  • -
  • +
  • -
    - Next +
    - -
  • - - + + + + +
    diff --git a/src/components/PastWeekPassedLearnersTable/data/hooks/usePastWeekPassedLearners.js b/src/components/PastWeekPassedLearnersTable/data/hooks/usePastWeekPassedLearners.js new file mode 100644 index 0000000000..579df42b3a --- /dev/null +++ b/src/components/PastWeekPassedLearnersTable/data/hooks/usePastWeekPassedLearners.js @@ -0,0 +1,95 @@ +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' }, + CourseTitle: { key: 'course_title' }, + PassedDate: { key: 'passed_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 usePastWeekPassedLearners = (enterpriseId) => { + const shouldTrackFetchEvents = useRef(false); + const [isLoading, setIsLoading] = useState(true); + const [pastWeekPassedLearners, setPastWeekPassedLearners] = useState({ + itemCount: 0, + pageCount: 0, + results: [], + }); + + const fetchPastWeekPassedLearners = useCallback(async (args) => { + try { + setIsLoading(true); + const options = { + page: args.pageIndex + 1, + pageSize: args.pageSize, + passedDate: 'last_week', + }; + applySortByToOptions(args.sortBy, options); + + const response = await EnterpriseDataApiService.fetchCourseEnrollments(enterpriseId, options); + const data = camelCaseObject(response.data); + setPastWeekPassedLearners({ + itemCount: data.count, + pageCount: data.numPages ?? Math.floor(data.count / options.pageSize), + results: data.results, + }); + + if (shouldTrackFetchEvents.current) { + // track event only after original API query to avoid sending event on initial page load. instead, + // only track event when user performs manual data operation (e.g., pagination, sort, filter) and + // send all table state as event properties. + sendEnterpriseTrackEvent( + enterpriseId, + EVENT_NAMES.PROGRESS_REPORT.DATATABLE_SORT_BY_OR_FILTER, + { + tableId: 'completed-learners-week', + ...options, + }, + ); + } else { + // set to true to enable tracking events on future API queries + shouldTrackFetchEvents.current = true; + } + } catch (error) { + logError(error); + } finally { + setIsLoading(false); + } + }, [enterpriseId]); + + const debouncedFetchPastWeekPassedLearners = useMemo( + () => debounce(fetchPastWeekPassedLearners, 300), + [fetchPastWeekPassedLearners], + ); + + return { + isLoading, + pastWeekPassedLearners, + fetchPastWeekPassedLearners: debouncedFetchPastWeekPassedLearners, + }; +}; + +export default usePastWeekPassedLearners; diff --git a/src/components/PastWeekPassedLearnersTable/index.jsx b/src/components/PastWeekPassedLearnersTable/index.jsx index 2fa60f8b62..6972f27cf6 100644 --- a/src/components/PastWeekPassedLearnersTable/index.jsx +++ b/src/components/PastWeekPassedLearnersTable/index.jsx @@ -1,66 +1,86 @@ import React from 'react'; - +import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; - -import TableContainer from '../../containers/TableContainer'; -import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; +import { DataTable } from '@openedx/paragon'; +import { connect } from 'react-redux'; +import usePastWeekPassedLearners from './data/hooks/usePastWeekPassedLearners'; import { i18nFormatTimestamp } from '../../utils'; -const PastWeekPassedLearnersTable = () => { - const intl = useIntl(); +const UserEmail = ({ row }) => ( + {row.original.userEmail} +); - const tableColumns = [ - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.past.week.passed.learners.table.user_email.column.heading', - defaultMessage: 'Email', - description: 'Column heading for the user email column in the past week passed learners table', - }), - key: 'user_email', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.past.week.passed.learners.table.course_title.column.heading', - defaultMessage: 'Course Title', - description: 'Column heading for the course title column in the past week passed learners table', - }), - key: 'course_title', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.past.week.passed.learners.table.passed_date.column.heading', - defaultMessage: 'Passed Date', - description: 'Column heading for the passed date column in the past week passed learners table', - }), - key: 'passed_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}, - passed_date: i18nFormatTimestamp({ intl, timestamp: learner.passed_date }), - })); +const PastWeekPassedLearnersTable = ({ enterpriseId }) => { + const intl = useIntl(); + const { + isLoading, + pastWeekPassedLearners: tableData, + fetchPastWeekPassedLearners: fetchTableData, + } = usePastWeekPassedLearners(enterpriseId); return ( - EnterpriseDataApiService.fetchCourseEnrollments( - enterpriseId, + i18nFormatTimestamp({ intl, timestamp: row.values.passedDate }), }, - )} - columns={tableColumns} - formatData={formatLearnerData} - tableSortable + ]} + initialState={{ + pageSize: 20, // Set this according to your requirements + pageIndex: 0, + sortBy: [{ id: 'passedDate', desc: true }], + selectedRowsOrdered: [], + }} + fetchData={fetchTableData} + data={tableData.results} + itemCount={tableData.itemCount} + pageCount={tableData.pageCount} /> ); }; -export default PastWeekPassedLearnersTable; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +PastWeekPassedLearnersTable.propTypes = { + enterpriseId: PropTypes.string.isRequired, +}; + +export default connect(mapStateToProps)(PastWeekPassedLearnersTable); 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;