From ba9e97cbd4c7c7306ba16262a5b6bfc95abe6e73 Mon Sep 17 00:00:00 2001 From: Cyril Nxumalo Date: Fri, 30 Aug 2024 10:15:54 +0200 Subject: [PATCH] chore: Migrate deprecated Table to DataTable for LearnerActivityTable --- .../Admin/__snapshots__/Admin.test.jsx.snap | 224 +- .../LearnerActivityTable.test.jsx | 130 +- .../LearnerActivityTable.test.jsx.snap | 2811 +++++++++++------ .../data/hooks/useCourseEnrollments.js | 99 + .../data/tests/constants.js | 38 + src/components/LearnerActivityTable/index.jsx | 280 +- src/data/constants/table.js | 4 + src/eventTracking.js | 6 + 8 files changed, 2356 insertions(+), 1236 deletions(-) create mode 100644 src/components/LearnerActivityTable/data/hooks/useCourseEnrollments.js create mode 100644 src/components/LearnerActivityTable/data/tests/constants.js diff --git a/src/components/Admin/__snapshots__/Admin.test.jsx.snap b/src/components/Admin/__snapshots__/Admin.test.jsx.snap index 93e82b2841..f0533bf7e4 100644 --- a/src/components/Admin/__snapshots__/Admin.test.jsx.snap +++ b/src/components/Admin/__snapshots__/Admin.test.jsx.snap @@ -1496,7 +1496,7 @@ exports[` renders correctly with dashboard analytics data renders # cou className="col-12 col-md-6 col-xl-4 pt-1 pb-3" > Showing data as of - July 31, 2018 + August 1, 2018
+ + + + +
@@ -499,35 +680,543 @@ exports[`LearnerActivityTable renders active learners table correctly 1`] = ` exports[`LearnerActivityTable renders empty state correctly 1`] = `
- - - - -
- There are no results. +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+ + + Email + + + + + + + + + + + Course Title + + + + + + + + + + + Course Price + + + + + + + + + + + Start Date + + + + + + + + + + + End Date + + + + + + + + + + + Passed Date + + + + + + + + + + + Current Grade + + + + + + + + + + + Progress Status + + + + + + + + + + + Last Activity Date + + + + + + + +
+
+
+ No results found +
+
+ +
@@ -535,253 +1224,412 @@ exports[`LearnerActivityTable renders empty state correctly 1`] = ` exports[`LearnerActivityTable renders inactive past month learners table correctly 1`] = `
- - +
+ Showing 1 - 2 of 2. +
+ +
+
+
+
+
+
+
+
+ @@ -891,101 +1740,105 @@ exports[`LearnerActivityTable renders inactive past month learners table correct
- + - + - + - + - + - + - + - +
Dive into ReactJS $200 October 21, 2017 May 13, 2018 66% Failed September 22, 2018
Redux with ReactJS $200 October 21, 2017 May 13, 2018 80% Passed September 25, 2018
-
-
-
-
-
+
    -
- - -
  • -
  • +
  • -
    - Next +
    - -
  • - - + + + + +
    @@ -993,253 +1846,412 @@ exports[`LearnerActivityTable renders inactive past month learners table correct exports[`LearnerActivityTable renders inactive past week learners table correctly 1`] = `
    - - +
    + Showing 1 - 2 of 2. +
    + +
    +
    +
    +
    +
    +
    +
    +
    + @@ -1349,101 +2362,105 @@ exports[`LearnerActivityTable renders inactive past week learners table correctl
    - + - + - + - + - + - + - + - +
    Dive into ReactJS $200 October 21, 2017 May 13, 2018 66% Failed September 22, 2018
    Redux with ReactJS $200 October 21, 2017 May 13, 2018 80% Passed September 25, 2018
    -
    -
    -
    -
    -
    - - -
  • -
  • +
  • -
    - Next +
    - -
  • - - + + + + +
    diff --git a/src/components/LearnerActivityTable/data/hooks/useCourseEnrollments.js b/src/components/LearnerActivityTable/data/hooks/useCourseEnrollments.js new file mode 100644 index 0000000000..e9d67c3e14 --- /dev/null +++ b/src/components/LearnerActivityTable/data/hooks/useCourseEnrollments.js @@ -0,0 +1,99 @@ +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' }, + courseListPrice: { key: 'course_list_price' }, + courseStartDate: { key: 'course_start_date' }, + courseEndDate: { key: 'course_end_date' }, + passedDate: { key: 'passed_date' }, + currentGrade: { key: 'current_grade' }, + progressStatus: { key: 'progress_status' }, + 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 useCourseEnrollments = (enterpriseId, tableId) => { + const shouldTrackFetchEvents = useRef(false); + const [isLoading, setIsLoading] = useState(true); + const [courseEnrollments, setCourseEnrollments] = useState({ + itemCount: 0, + pageCount: 0, + results: [], + }); + + const fetchCourseEnrollments = useCallback(async (args) => { + try { + setIsLoading(true); + const options = { + page: args.pageIndex + 1, + pageSize: args.pageSize, + }; + applySortByToOptions(args.sortBy, options); + + const response = await EnterpriseDataApiService.fetchCourseEnrollments(enterpriseId, options); + const data = camelCaseObject(response.data); + setCourseEnrollments({ + 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, + ...options, + }, + ); + } else { + // set to true to enable tracking events on future API queries + shouldTrackFetchEvents.current = true; + } + } catch (error) { + logError(error); + } finally { + setIsLoading(false); + } + }, [enterpriseId, tableId]); + + const debouncedFetchCourseEnrollments = useMemo( + () => debounce(fetchCourseEnrollments, 300), + [fetchCourseEnrollments], + ); + + return { + isLoading, + courseEnrollments, + fetchCourseEnrollments: debouncedFetchCourseEnrollments, + }; +}; + +export default useCourseEnrollments; diff --git a/src/components/LearnerActivityTable/data/tests/constants.js b/src/components/LearnerActivityTable/data/tests/constants.js new file mode 100644 index 0000000000..7928440642 --- /dev/null +++ b/src/components/LearnerActivityTable/data/tests/constants.js @@ -0,0 +1,38 @@ +const mockUseCourseEnrollments = { + isLoading: false, + courseEnrollments: { + itemCount: 2, + pageCount: 1, + results: [ + { + id: 1, + passedDate: '2018-09-23', + courseTitle: 'Dive into ReactJS', + courseKey: 'edX/ReactJS', + userEmail: 'awesome.me@example.com', + courseListPrice: '200', + courseStartDate: '2017-10-21', + courseEndDate: '2018-05-13', + currentGrade: '0.66', + progressStatus: 'Failed', + lastActivityDate: '2018-09-22', + }, + { + id: 5, + passedDate: '2018-09-22', + courseTitle: 'Redux with ReactJS', + courseKey: 'edX/Redux_ReactJS', + userEmail: 'new@example.com', + courseListPrice: '200', + courseStartDate: '2017-10-21', + courseEndDate: '2018-05-13', + currentGrade: '0.80', + progressStatus: 'Passed', + lastActivityDate: '2018-09-25', + }, + ], + }, + fetchCourseEnrollments: jest.fn(), +}; + +export default mockUseCourseEnrollments; diff --git a/src/components/LearnerActivityTable/index.jsx b/src/components/LearnerActivityTable/index.jsx index 114763aaa7..318beb468d 100644 --- a/src/components/LearnerActivityTable/index.jsx +++ b/src/components/LearnerActivityTable/index.jsx @@ -1,153 +1,159 @@ import React from 'react'; import PropTypes from 'prop-types'; - -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; - -import TableContainer from '../../containers/TableContainer'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { DataTable, TextFilter } from '@openedx/paragon'; +import { connect } from 'react-redux'; +import useCourseEnrollments from './data/hooks/useCourseEnrollments'; +import { DEFAULT_PAGE, PAGE_SIZE } from '../../data/constants/table'; import { - i18nFormatTimestamp, i18nFormatPassedTimestamp, i18nFormatProgressStatus, formatPercentage, + i18nFormatTimestamp, + i18nFormatPassedTimestamp, + i18nFormatProgressStatus, + formatPercentage, } from '../../utils'; -import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; +import { formatPrice } from '../learner-credit-management/data'; -class LearnerActivityTable extends React.Component { - getTableColumns() { - const { activity, intl } = this.props; - const tableColumns = [ - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.learner.activity.table.user_email.column.heading', - defaultMessage: 'Email', - description: 'Column heading for the user email column in the learner activity table', - }), - key: 'user_email', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.learner.activity.table.course_title.column.heading', - defaultMessage: 'Course Title', - description: 'Column heading for the course title column in the learner activity table', - }), - key: 'course_title', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.learner.activity.table.course_list_price.column.heading', - defaultMessage: 'Course Price', - description: 'Column heading for the course price column in the learner activity table', - }), - key: 'course_list_price', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.learner.activity.table.course_start_date.column.heading', - defaultMessage: 'Start Date', - description: 'Column heading for the course start date column in the learner activity table', - }), - key: 'course_start_date', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.learner.activity.table.course_end_date.column.heading', - defaultMessage: 'End Date', - description: 'Column heading for the course end date column in the learner activity table', - }), - key: 'course_end_date', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.learner.activity.table.passed_date.column.heading', - defaultMessage: 'Passed Date', - description: 'Column heading for the passed date column in the learner activity table', - }), - key: 'passed_date', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.learner.activity.table.current_grade.column.heading', - defaultMessage: 'Current Grade', - description: 'Column heading for the current grade column in the learner activity table', - }), - key: 'current_grade', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.learner.activity.table.progress_status.column.heading', - defaultMessage: 'Progress Status', - description: 'Column heading for the progress status column in the learner activity table', - }), - key: 'progress_status', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.learner.activity.table.enrollment_date.column.heading', - defaultMessage: 'Last Activity Date', - description: 'Column heading for the last activity date column in the learner activity table', - }), - key: 'last_activity_date', - columnSortable: true, - }, - ]; +const FilterStatus = (rest) => ; - if (activity !== 'active_past_week') { - return tableColumns.filter(column => column.key !== 'passed_date'); - } - return tableColumns; - } +const UserEmail = ({ row }) => ( + {row.original.userEmail} +); + +UserEmail.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + userEmail: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; - formatTableData = enrollments => enrollments.map(enrollment => ({ - ...enrollment, - user_email: {enrollment.user_email}, - last_activity_date: i18nFormatTimestamp({ intl: this.props.intl, timestamp: enrollment.last_activity_date }), - course_start_date: i18nFormatTimestamp({ intl: this.props.intl, timestamp: enrollment.course_start_date }), - course_end_date: i18nFormatTimestamp({ intl: this.props.intl, timestamp: enrollment.course_end_date }), - enrollment_date: i18nFormatTimestamp({ - intl: this.props.intl, timestamp: enrollment.enrollment_date, - }), - passed_date: i18nFormatPassedTimestamp({ intl: this.props.intl, timestamp: enrollment.passed_date }), - user_account_creation_date: i18nFormatTimestamp({ - intl: this.props.intl, timestamp: enrollment.user_account_creation_date, - }), - progress_status: i18nFormatProgressStatus({ intl: this.props.intl, progressStatus: enrollment.progress_status }), - course_list_price: enrollment.course_list_price ? `$${enrollment.course_list_price}` : '', - current_grade: formatPercentage({ decimal: enrollment.current_grade }), - })); +const LearnerActivityTable = ({ id, enterpriseId, activity }) => { + const intl = useIntl(); + const { + isLoading, + courseEnrollments: tableData, + fetchCourseEnrollments: fetchTableData, + } = useCourseEnrollments(enterpriseId, id); - render() { - const { activity, id } = this.props; + const columns = [ + { + Header: intl.formatMessage({ + id: 'admin.portal.lpr.learner.activity.table.user_email.column.heading', + defaultMessage: 'Email', + description: 'Column heading for the user email column in the learner activity table', + }), + accessor: 'userEmail', + Cell: UserEmail, + }, + { + Header: intl.formatMessage({ + id: 'admin.portal.lpr.learner.activity.table.course_title.column.heading', + defaultMessage: 'Course Title', + description: 'Column heading for the course title column in the learner activity table', + }), + accessor: 'courseTitle', + }, + { + Header: intl.formatMessage({ + id: 'admin.portal.lpr.learner.activity.table.course_list_price.column.heading', + defaultMessage: 'Course Price', + description: 'Column heading for the course price column in the learner activity table', + }), + accessor: 'courseListPrice', + Cell: ({ row }) => formatPrice(row.values.courseListPrice), + }, + { + Header: intl.formatMessage({ + id: 'admin.portal.lpr.learner.activity.table.course_start_date.column.heading', + defaultMessage: 'Start Date', + description: 'Column heading for the course start date column in the learner activity table', + }), + accessor: 'courseStartDate', + Cell: ({ row }) => i18nFormatTimestamp({ intl, timestamp: row.values.courseStartDate }), + }, + { + Header: intl.formatMessage({ + id: 'admin.portal.lpr.learner.activity.table.course_end_date.column.heading', + defaultMessage: 'End Date', + description: 'Column heading for the course end date column in the learner activity table', + }), + accessor: 'courseEndDate', + Cell: ({ row }) => i18nFormatTimestamp({ intl, timestamp: row.values.courseEndDate }), + }, + { + Header: intl.formatMessage({ + id: 'admin.portal.lpr.learner.activity.table.passed_date.column.heading', + defaultMessage: 'Passed Date', + description: 'Column heading for the passed date column in the learner activity table', + }), + accessor: 'passedDate', + Cell: ({ row }) => i18nFormatPassedTimestamp({ intl, timestamp: row.values.passedDate }), + }, + { + Header: intl.formatMessage({ + id: 'admin.portal.lpr.learner.activity.table.current_grade.column.heading', + defaultMessage: 'Current Grade', + description: 'Column heading for the current grade column in the learner activity table', + }), + accessor: 'currentGrade', + Cell: ({ row }) => formatPercentage({ decimal: row.values.currentGrade }), + }, + { + Header: intl.formatMessage({ + id: 'admin.portal.lpr.learner.activity.table.progress_status.column.heading', + defaultMessage: 'Progress Status', + description: 'Column heading for the progress status column in the learner activity table', + }), + accessor: 'progressStatus', + Cell: ({ row }) => i18nFormatProgressStatus({ intl, progressStatus: row.values.progressStatus }), + }, + { + Header: intl.formatMessage({ + id: 'admin.portal.lpr.learner.activity.table.enrollment_date.column.heading', + defaultMessage: 'Last Activity Date', + description: 'Column heading for the last activity date column in the learner activity table', + }), + accessor: 'lastActivityDate', + Cell: ({ row }) => i18nFormatTimestamp({ intl, timestamp: row.values.lastActivityDate }), + }, + ]; - return ( - EnterpriseDataApiService.fetchCourseEnrollments( - enterpriseId, - { - learnerActivity: activity, - ...options, - }, - )} - columns={this.getTableColumns()} - formatData={this.formatTableData} - tableSortable - /> - ); + if (activity !== 'active_past_week') { + columns.splice(columns.findIndex(col => col.accessor === 'passedDate'), 1); } -} + + return ( + + ); +}; + +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); LearnerActivityTable.propTypes = { id: PropTypes.string.isRequired, + enterpriseId: PropTypes.string.isRequired, activity: PropTypes.string.isRequired, - // injected - intl: intlShape.isRequired, }; -export default injectIntl(LearnerActivityTable); +export default connect(mapStateToProps)(LearnerActivityTable); diff --git a/src/data/constants/table.js b/src/data/constants/table.js index aeb1588897..d83ac45f3d 100644 --- a/src/data/constants/table.js +++ b/src/data/constants/table.js @@ -5,6 +5,8 @@ const SORT_REQUEST = 'SORT_REQUEST'; const SORT_SUCCESS = 'SORT_SUCCESS'; const SORT_FAILURE = 'SORT_FAILURE'; const CLEAR_TABLE = 'CLEAR_TABLE'; +const PAGE_SIZE = 20; +const DEFAULT_PAGE = 0; export { PAGINATION_REQUEST, @@ -14,4 +16,6 @@ export { SORT_SUCCESS, SORT_FAILURE, CLEAR_TABLE, + PAGE_SIZE, + DEFAULT_PAGE, }; 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;