diff --git a/src/components/EnrolledLearnersTable/__snapshots__/EnrolledLearnersTable.test.jsx.snap b/src/components/EnrolledLearnersTable/__snapshots__/EnrolledLearnersTable.test.jsx.snap index 829f004573..eee3bba458 100644 --- a/src/components/EnrolledLearnersTable/__snapshots__/EnrolledLearnersTable.test.jsx.snap +++ b/src/components/EnrolledLearnersTable/__snapshots__/EnrolledLearnersTable.test.jsx.snap @@ -2,35 +2,328 @@ exports[`EnrolledLearnersTable renders empty state correctly 1`] = `
- - - - -
- There are no results. +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + loading + +
+
+ + + + + + + + + +
+ + + Email + + + + + + + + + + + Account Created + + + + + + + + + + + Total Course Enrollment Count + + + + + + + +
+
+
+ +
diff --git a/src/components/EnrolledLearnersTable/data/hooks/useEnrolledLearners.js b/src/components/EnrolledLearnersTable/data/hooks/useEnrolledLearners.js new file mode 100644 index 0000000000..2944057a5f --- /dev/null +++ b/src/components/EnrolledLearnersTable/data/hooks/useEnrolledLearners.js @@ -0,0 +1,106 @@ +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 = { + lmsUserCreated: { key: 'lms_user_created' }, + userEmail: { key: 'user_email' }, + enrollmentCount: { key: 'enrollment_count' }, + }; + 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 applyFiltersToOptions = (filters, options) => { + if (!filters || filters.length === 0) { + return; + } + const userEmailFilter = filters.find(filter => filter.id === 'userEmail')?.value; + if (userEmailFilter) { + Object.assign(options, { userEmail: userEmailFilter }); + } +}; + +const useEnrolledLearners = (enterpriseId) => { + const shouldTrackFetchEvents = useRef(false); + const [isLoading, setIsLoading] = useState(true); + const [enrolledLearners, setEnrolledLearners] = useState( + { + itemCount: 0, + pageCount: 0, + results: [], + }, + ); + + const fetchEnrolledLearners = useCallback(async (args) => { + try { + setIsLoading(true); + const options = { + page: args.pageIndex + 1, + pageSize: args.pageSize, + }; + applySortByToOptions(args.sortBy, options); + applyFiltersToOptions(args.filters, options); + + const response = await EnterpriseDataApiService.fetchEnrolledLearners(enterpriseId, options); + const data = camelCaseObject(response.data); + setEnrolledLearners({ + 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: 'enrolled-learners', + ...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 debouncedFetchEnrolledLearners = useMemo( + () => debounce(fetchEnrolledLearners, 300), + [fetchEnrolledLearners], + ); + + return { + isLoading, + enrolledLearners, + fetchEnrolledLearners: debouncedFetchEnrolledLearners, + }; +}; + +export default useEnrolledLearners; diff --git a/src/components/EnrolledLearnersTable/index.jsx b/src/components/EnrolledLearnersTable/index.jsx index e4e40e9103..8f33249170 100644 --- a/src/components/EnrolledLearnersTable/index.jsx +++ b/src/components/EnrolledLearnersTable/index.jsx @@ -2,61 +2,97 @@ import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; -import TableContainer from '../../containers/TableContainer'; +import { DataTable, TextFilter } from '@openedx/paragon'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { DEFAULT_PAGE, PAGE_SIZE } from '../learner-credit-management/data'; +import useEnrolledLearners from './data/hooks/useEnrolledLearners'; import { i18nFormatTimestamp } from '../../utils'; -import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; -const EnrolledLearnersTable = () => { - const intl = useIntl(); +const FilterStatus = (rest) => ; + +const UserEmail = ({ row }) => ( + {row.original.userEmail} +); + +UserEmail.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + userEmail: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; - const tableColumns = [ - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.enrolled.learners.table.user_email.column.heading', - defaultMessage: 'Email', - description: 'Column heading for the user email column in the enrolled learners table', - }), - key: 'user_email', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.enrolled.learners.table.lms_user_created.column.heading', - defaultMessage: 'Account Created', - description: 'Column heading for the lms user created column in the enrolled learners table', - }), - key: 'lms_user_created', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.enrolled.learners.table.enrollment_count.column.heading', - defaultMessage: 'Total Course Enrollment Count', - description: 'Column heading for the course enrollment count column in the enrolled learners table', - }), - key: 'enrollment_count', - columnSortable: true, - }, - ]; - - const formatLearnerData = learners => learners.map(learner => ({ - ...learner, - user_email: {learner.user_email}, - lms_user_created: i18nFormatTimestamp({ - intl, timestamp: learner.lms_user_created, - }), - })); +const EnrolledLearnersTable = ({ enterpriseId }) => { + const intl = useIntl(); + const { + isLoading, + enrolledLearners: tableData, + fetchEnrolledLearners: fetchTableData, + } = useEnrolledLearners(enterpriseId); return ( - i18nFormatTimestamp({ intl, timestamp: row.values.lmsUserCreated }), + disableFilters: true, + }, + { + Header: intl.formatMessage({ + id: 'admin.portal.lpr.enrolled.learners.table.enrollment_count.column.heading', + defaultMessage: 'Total Course Enrollment Count', + description: 'Column heading for the course enrollment count column in the enrolled learners table', + }), + accessor: 'enrollmentCount', + disableFilters: true, + }, + ]} + initialState={{ + pageSize: PAGE_SIZE, + pageIndex: DEFAULT_PAGE, + sortBy: [ + { id: 'lmsUserCreated', desc: true }, + ], + }} + fetchData={fetchTableData} + data={tableData.results} + itemCount={tableData.itemCount} + pageCount={tableData.pageCount} /> ); }; -export default EnrolledLearnersTable; +const mapStateToProps = state => ({ + enterpriseId: state.portalConfiguration.enterpriseId, +}); + +EnrolledLearnersTable.propTypes = { + enterpriseId: PropTypes.string.isRequired, +}; + +export default connect(mapStateToProps)(EnrolledLearnersTable); 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;