diff --git a/src/components/Admin/CompletedLearnersTable.jsx b/src/components/Admin/CompletedLearnersTable.jsx new file mode 100644 index 0000000000..2842139bfc --- /dev/null +++ b/src/components/Admin/CompletedLearnersTable.jsx @@ -0,0 +1,64 @@ +import React, { useMemo } from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { DataTable } from '@openedx/paragon'; +import { useGenericTableData } from './data/hooks'; +import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; +import EmailCell from './EmailCell'; + +const CompletedLearnersTable = (enterpriseId) => { + const intl = useIntl(); + const { + isLoading, + tableData, + fetchTableData, + } = useGenericTableData( + enterpriseId, + 'completed-learners', + EnterpriseDataApiService.fetchCompletedLearners, + useMemo(() => ({ + userEmail: { key: 'user_email' }, + completedCourses: { key: 'completed_courses' }, + }), []), + ); + + return ( + + ); +}; + +export default CompletedLearnersTable; diff --git a/src/components/CompletedLearnersTable/CompletedLearnersTable.test.jsx b/src/components/Admin/CompletedLearnersTable.test.jsx similarity index 72% rename from src/components/CompletedLearnersTable/CompletedLearnersTable.test.jsx rename to src/components/Admin/CompletedLearnersTable.test.jsx index ece4178180..3a79c64fb8 100644 --- a/src/components/CompletedLearnersTable/CompletedLearnersTable.test.jsx +++ b/src/components/Admin/CompletedLearnersTable.test.jsx @@ -6,26 +6,20 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; -import CompletedLearnersTable from '.'; +import CompletedLearnersTable from './CompletedLearnersTable'; +import { useGenericTableData } from './data/hooks'; const mockStore = configureMockStore([thunk]); const enterpriseId = 'test-enterprise'; + +jest.mock('./data/hooks/useGenericTableData', () => ( + jest.fn().mockReturnValue({}) +)); + const store = mockStore({ portalConfiguration: { enterpriseId, }, - table: { - 'completed-learners': { - data: { - results: [], - current_page: 1, - num_pages: 1, - }, - ordering: null, - loading: false, - error: null, - }, - }, }); const CompletedLearnersWrapper = props => ( @@ -42,6 +36,15 @@ const CompletedLearnersWrapper = props => ( describe('CompletedLearnersTable', () => { it('renders empty state correctly', () => { + useGenericTableData.mockReturnValue({ + isLoading: false, + tableData: { + results: [], + itemCount: 0, + pageCount: 0, + }, + fetchTableData: jest.fn(), + }); const tree = renderer .create(( diff --git a/src/components/Admin/EmailCell.jsx b/src/components/Admin/EmailCell.jsx new file mode 100644 index 0000000000..cf97e72ef3 --- /dev/null +++ b/src/components/Admin/EmailCell.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const EmailCell = ({ row }) => ( + {row.original.userEmail} +); + +EmailCell.propTypes = { + row: PropTypes.shape({ + original: PropTypes.shape({ + userEmail: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; + +export default EmailCell; diff --git a/src/components/Admin/EmailCell.test.jsx b/src/components/Admin/EmailCell.test.jsx new file mode 100644 index 0000000000..f2c69eda0d --- /dev/null +++ b/src/components/Admin/EmailCell.test.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import EmailCell from './EmailCell'; + +describe('Email Component', () => { + it('should display the user email and suppress highlighting', () => { + const mockRow = { + original: { + userEmail: 'test@example.com', + }, + }; + + render( + , + ); + + // Assert that the email is rendered correctly + const emailElement = screen.getByText('test@example.com'); + expect(emailElement).toBeInTheDocument(); + + // Assert that data-hj-suppress is present to suppress highlighting + expect(emailElement).toHaveAttribute('data-hj-suppress'); + }); + + it('should throw a prop-type warning if the email is not provided', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + const invalidRow = { + original: {}, + }; + + // Render with invalid props + render(); + + // Assert that a prop-types error has been logged + expect(consoleSpy).toHaveBeenCalled(); + + // Clean up the spy + consoleSpy.mockRestore(); + }); +}); diff --git a/src/components/Admin/__snapshots__/Admin.test.jsx.snap b/src/components/Admin/__snapshots__/Admin.test.jsx.snap index 93e82b2841..f7f4b78f98 100644 --- a/src/components/Admin/__snapshots__/Admin.test.jsx.snap +++ b/src/components/Admin/__snapshots__/Admin.test.jsx.snap @@ -1621,7 +1621,7 @@ exports[` renders correctly with dashboard analytics data renders # cou > Filter by course @@ -1630,7 +1630,7 @@ exports[` renders correctly with dashboard analytics data renders # cou > renders correctly with dashboard analytics data renders # cou > Filter by start date @@ -1688,7 +1688,7 @@ exports[` renders correctly with dashboard analytics data renders # cou renders correctly with dashboard analytics data renders # cou > renders correctly with dashboard analytics data renders # cou className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-19" + id="pgn-searchfield-input-22" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -1811,7 +1811,7 @@ exports[` renders correctly with dashboard analytics data renders # cou > renders correctly with dashboard analytics data renders # cou renders correctly with dashboard analytics data renders # of > Filter by course @@ -3175,7 +3175,7 @@ exports[` renders correctly with dashboard analytics data renders # of > renders correctly with dashboard analytics data renders # of > Filter by start date @@ -3233,7 +3233,7 @@ exports[` renders correctly with dashboard analytics data renders # of renders correctly with dashboard analytics data renders # of > renders correctly with dashboard analytics data renders # of className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-54" + id="pgn-searchfield-input-62" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -3356,7 +3356,7 @@ exports[` renders correctly with dashboard analytics data renders # of > renders correctly with dashboard analytics data renders # of renders correctly with dashboard analytics data renders # of > Filter by course @@ -4720,7 +4720,7 @@ exports[` renders correctly with dashboard analytics data renders # of > renders correctly with dashboard analytics data renders # of > Filter by start date @@ -4778,7 +4778,7 @@ exports[` renders correctly with dashboard analytics data renders # of renders correctly with dashboard analytics data renders # of > renders correctly with dashboard analytics data renders # of className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-61" + id="pgn-searchfield-input-70" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -4901,7 +4901,7 @@ exports[` renders correctly with dashboard analytics data renders # of > renders correctly with dashboard analytics data renders # of renders correctly with dashboard analytics data renders colla > Filter by course @@ -6265,7 +6265,7 @@ exports[` renders correctly with dashboard analytics data renders colla > renders correctly with dashboard analytics data renders colla > Filter by start date @@ -6323,7 +6323,7 @@ exports[` renders correctly with dashboard analytics data renders colla renders correctly with dashboard analytics data renders colla > renders correctly with dashboard analytics data renders colla className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-68" + id="pgn-searchfield-input-78" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -6446,7 +6446,7 @@ exports[` renders correctly with dashboard analytics data renders colla > renders correctly with dashboard analytics data renders colla renders correctly with dashboard analytics data renders full > Filter by course @@ -7810,7 +7810,7 @@ exports[` renders correctly with dashboard analytics data renders full > renders correctly with dashboard analytics data renders full > Filter by start date @@ -7868,7 +7868,7 @@ exports[` renders correctly with dashboard analytics data renders full renders correctly with dashboard analytics data renders full > renders correctly with dashboard analytics data renders full className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-5" + id="pgn-searchfield-input-6" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -7991,7 +7991,7 @@ exports[` renders correctly with dashboard analytics data renders full > renders correctly with dashboard analytics data renders full renders correctly with dashboard analytics data renders inact > Filter by course @@ -9355,7 +9355,7 @@ exports[` renders correctly with dashboard analytics data renders inact > renders correctly with dashboard analytics data renders inact > Filter by start date @@ -9413,7 +9413,7 @@ exports[` renders correctly with dashboard analytics data renders inact renders correctly with dashboard analytics data renders inact > renders correctly with dashboard analytics data renders inact className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-47" + id="pgn-searchfield-input-54" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -9536,7 +9536,7 @@ exports[` renders correctly with dashboard analytics data renders inact > renders correctly with dashboard analytics data renders inact renders correctly with dashboard analytics data renders inact > Filter by course @@ -10900,7 +10900,7 @@ exports[` renders correctly with dashboard analytics data renders inact > renders correctly with dashboard analytics data renders inact > Filter by start date @@ -10958,7 +10958,7 @@ exports[` renders correctly with dashboard analytics data renders inact renders correctly with dashboard analytics data renders inact > renders correctly with dashboard analytics data renders inact className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-40" + id="pgn-searchfield-input-46" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -11081,7 +11081,7 @@ exports[` renders correctly with dashboard analytics data renders inact > renders correctly with dashboard analytics data renders inact renders correctly with dashboard analytics data renders learn > Filter by course @@ -12445,7 +12445,7 @@ exports[` renders correctly with dashboard analytics data renders learn > renders correctly with dashboard analytics data renders learn > Filter by start date @@ -12503,7 +12503,7 @@ exports[` renders correctly with dashboard analytics data renders learn renders correctly with dashboard analytics data renders learn > renders correctly with dashboard analytics data renders learn className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-26" + id="pgn-searchfield-input-30" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -12626,7 +12626,7 @@ exports[` renders correctly with dashboard analytics data renders learn > renders correctly with dashboard analytics data renders learn renders correctly with dashboard analytics data renders regis > Filter by course @@ -13990,7 +13990,7 @@ exports[` renders correctly with dashboard analytics data renders regis > renders correctly with dashboard analytics data renders regis > Filter by start date @@ -14048,7 +14048,7 @@ exports[` renders correctly with dashboard analytics data renders regis renders correctly with dashboard analytics data renders regis > renders correctly with dashboard analytics data renders regis className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-12" + id="pgn-searchfield-input-14" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -14171,7 +14171,7 @@ exports[` renders correctly with dashboard analytics data renders regis > renders correctly with dashboard analytics data renders regis renders correctly with dashboard analytics data renders top a > Filter by course @@ -15535,7 +15535,7 @@ exports[` renders correctly with dashboard analytics data renders top a > renders correctly with dashboard analytics data renders top a > Filter by start date @@ -15593,7 +15593,7 @@ exports[` renders correctly with dashboard analytics data renders top a renders correctly with dashboard analytics data renders top a > renders correctly with dashboard analytics data renders top a className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-33" + id="pgn-searchfield-input-38" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -15716,7 +15716,7 @@ exports[` renders correctly with dashboard analytics data renders top a > renders correctly with dashboard analytics data renders top a renders correctly with dashboard insights data renders dashbo > Filter by course @@ -17129,7 +17129,7 @@ exports[` renders correctly with dashboard insights data renders dashbo > renders correctly with dashboard insights data renders dashbo > Filter by start date @@ -17187,7 +17187,7 @@ exports[` renders correctly with dashboard insights data renders dashbo renders correctly with dashboard insights data renders dashbo > renders correctly with dashboard insights data renders dashbo className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-86" + id="pgn-searchfield-input-100" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -17310,7 +17310,7 @@ exports[` renders correctly with dashboard insights data renders dashbo > renders correctly with dashboard insights data renders dashbo renders correctly with error state 1`] = ` > renders correctly with error state 1`] = ` renders correctly with loading state 1`] = ` > renders correctly with loading state 1`] = ` renders correctly with no dashboard insights data 1`] = ` > Filter by course @@ -20396,7 +20396,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` > renders correctly with no dashboard insights data 1`] = ` > Filter by start date @@ -20454,7 +20454,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` renders correctly with no dashboard insights data 1`] = ` > renders correctly with no dashboard insights data 1`] = ` className="form-control" data-hj-suppress={true} disabled={false} - id="pgn-searchfield-input-79" + id="pgn-searchfield-input-92" name="searchfield-input" onBlur={[Function]} onChange={[Function]} @@ -20577,7 +20577,7 @@ exports[` renders correctly with no dashboard insights data 1`] = ` > renders correctly with no dashboard insights data 1`] = ` + + + + + + + + + + + + + + + + + + + Email + + + + + + + + + + + + Total Course Completed Count + + + + + + + + + + + + + + + No results found + + + + + Page 21, Current Page, of -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/src/components/Admin/data/hooks/index.js b/src/components/Admin/data/hooks/index.js index 0932cd221d..43104c3661 100644 --- a/src/components/Admin/data/hooks/index.js +++ b/src/components/Admin/data/hooks/index.js @@ -1,55 +1,2 @@ -import { useEffect, useState } from 'react'; -import { logError } from '@edx/frontend-platform/logging'; -import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; - -const useModuleActivityReport = ({ - enterpriseId, page, filters, searchQuery, -}) => { - const [isLoading, setIsLoading] = useState(true); - - const [paginationData, setPaginationData] = useState({ - itemCount: 0, - pageCount: 0, - data: [], - }); - - useEffect(() => { - // Reset the loading state - setIsLoading(true); - - EnterpriseDataApiService.fetchEnterpriseModuleActivityReport(enterpriseId, { - page: page + 1, - search: searchQuery, - ...filters, - }) - .then((response) => { - setPaginationData({ - itemCount: response.data.count, - pageCount: response.data.num_pages, - data: response.data.results, - currentPage: response.data.currentPage, - }); - - // Reset the loading state - setIsLoading(false); - }) - .catch((err) => { - logError(err); - - // Reset the loading state - setIsLoading(false); - }); - }, [ - enterpriseId, - page, - filters, - searchQuery, - ]); - - return { - isLoading, - paginationData, - }; -}; - -export default useModuleActivityReport; +export { default as useGenericTableData } from './useGenericTableData'; +export { default as useModuleActivityReport } from './useModuleActivityReport'; diff --git a/src/components/Admin/data/hooks/useGenericTableData.js b/src/components/Admin/data/hooks/useGenericTableData.js new file mode 100644 index 0000000000..ed38940a00 --- /dev/null +++ b/src/components/Admin/data/hooks/useGenericTableData.js @@ -0,0 +1,86 @@ +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 EVENT_NAMES from '../../../../eventTracking'; + +const applySortByToOptions = (sortBy, options, apiFieldsForColumnAccessor) => { + if (!sortBy || sortBy.length === 0) { + return; + } + 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 useGenericTableData = (enterpriseId, tableId, fetchMethod, apiFields) => { + const shouldTrackFetchEvents = useRef(false); + const [isLoading, setIsLoading] = useState(true); + const [tableData, setTableData] = useState({ + itemCount: 0, + pageCount: 0, + results: [], + }); + + const fetchTableData = useCallback(async (args) => { + try { + setIsLoading(true); + const options = { + page: args.pageIndex + 1, + pageSize: args.pageSize, + }; + applySortByToOptions(args.sortBy, options, apiFields); + + const response = await fetchMethod(enterpriseId, options); + const data = camelCaseObject(response.data); + + setTableData({ + 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, tableId, fetchMethod, apiFields]); + + const debouncedFetchTableData = useMemo( + () => debounce(fetchTableData, 300), + [fetchTableData], + ); + + return { + isLoading, + tableData, + fetchTableData: debouncedFetchTableData, + }; +}; + +export default useGenericTableData; diff --git a/src/components/Admin/data/hooks/useGenericTableData.test.js b/src/components/Admin/data/hooks/useGenericTableData.test.js new file mode 100644 index 0000000000..4800204052 --- /dev/null +++ b/src/components/Admin/data/hooks/useGenericTableData.test.js @@ -0,0 +1,157 @@ +import { renderHook } from '@testing-library/react-hooks'; +import debounce from 'lodash.debounce'; +import { logError } from '@edx/frontend-platform/logging'; +import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import EVENT_NAMES from '../../../../eventTracking'; +import useGenericTableData from './useGenericTableData'; + +jest.mock('lodash.debounce', () => jest.fn(fn => fn)); +jest.mock('@edx/frontend-platform/logging', () => ({ + logError: jest.fn(), +})); +jest.mock('@edx/frontend-enterprise-utils', () => { + const originalModule = jest.requireActual('@edx/frontend-enterprise-utils'); + return ({ + ...originalModule, + sendEnterpriseTrackEvent: jest.fn(), + }); +}); + +const mockFetchMethod = jest.fn(); +const mockEnterpriseId = 'test-enterprise-id'; +const mockTableId = 'test-table-id'; +const mockApiFields = { + userEmail: { key: 'user_email' }, + completedCourses: { key: 'completed_courses' }, +}; + +describe('useGenericTableData hook', () => { + beforeEach(() => { + mockFetchMethod.mockReset(); + sendEnterpriseTrackEvent.mockReset(); + debounce.mockClear(); + }); + + it('should initialize with loading state and empty data', () => { + const { result } = renderHook(() => useGenericTableData( + mockEnterpriseId, + mockTableId, + mockFetchMethod, + mockApiFields, + )); + + expect(result.current.isLoading).toBe(true); + + expect(result.current.tableData).toEqual({ + itemCount: 0, + pageCount: 0, + results: [], + }); + }); + + it('should call fetchMethod and update state when fetchTableData is invoked', async () => { + const mockResponseData = { + data: { + count: 2, + numPages: 1, + results: [{ user_email: 'test@example.com', completed_courses: 5 }], + }, + }; + + mockFetchMethod.mockResolvedValueOnce(mockResponseData); + + const { result, waitForNextUpdate } = renderHook(() => useGenericTableData( + mockEnterpriseId, + mockTableId, + mockFetchMethod, + mockApiFields, + )); + + result.current.fetchTableData({ pageIndex: 0, pageSize: 10, sortBy: [] }); + + await waitForNextUpdate(); + + expect(mockFetchMethod).toHaveBeenCalledWith(mockEnterpriseId, { page: 1, pageSize: 10 }); + expect(result.current.isLoading).toBe(false); + expect(result.current.tableData).toEqual({ + itemCount: 2, + pageCount: 1, + results: [{ userEmail: 'test@example.com', completedCourses: 5 }], + }); + }); + + it('should handle API errors and log the error', async () => { + const mockError = new Error('API failed'); + mockFetchMethod.mockRejectedValueOnce(mockError); + + const { result, waitForNextUpdate } = renderHook(() => useGenericTableData( + mockEnterpriseId, + mockTableId, + mockFetchMethod, + mockApiFields, + )); + + result.current.fetchTableData({ pageIndex: 0, pageSize: 10, sortBy: [] }); + + await waitForNextUpdate(); + + expect(logError).toHaveBeenCalledWith(mockError); + expect(result.current.isLoading).toBe(false); + expect(result.current.tableData).toEqual({ + itemCount: 0, + pageCount: 0, + results: [], + }); + }); + + it('should debounce fetchTableData', async () => { + const mockDebounce = jest.fn(fn => fn); + debounce.mockImplementationOnce(mockDebounce); + + const { result } = renderHook(() => useGenericTableData( + mockEnterpriseId, + mockTableId, + mockFetchMethod, + mockApiFields, + )); + + expect(debounce).toHaveBeenCalled(); + expect(typeof result.current.fetchTableData).toBe('function'); + }); + + it('should track event after fetching data', async () => { + const mockResponseData = { + data: { + count: 2, + numPages: 1, + results: [{ user_email: 'test@example.com', completed_courses: 5 }], + }, + }; + mockFetchMethod.mockResolvedValue(mockResponseData); + + const { result, waitForNextUpdate } = renderHook(() => useGenericTableData( + mockEnterpriseId, + mockTableId, + mockFetchMethod, + mockApiFields, + )); + + // First fetch (will not track the event) + result.current.fetchTableData({ pageIndex: 0, pageSize: 10, sortBy: [] }); + // // Second fetch (should track the event) + result.current.fetchTableData({ pageIndex: 1, pageSize: 10, sortBy: [] }); + + await waitForNextUpdate(); + + expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1); + expect(sendEnterpriseTrackEvent).toHaveBeenCalledWith( + mockEnterpriseId, + EVENT_NAMES.PROGRESS_REPORT.DATATABLE_SORT_BY_OR_FILTER, + { + tableId: mockTableId, + page: 2, + pageSize: 10, + }, + ); + }); +}); diff --git a/src/components/Admin/data/hooks/useModuleActivityReport.js b/src/components/Admin/data/hooks/useModuleActivityReport.js new file mode 100644 index 0000000000..0932cd221d --- /dev/null +++ b/src/components/Admin/data/hooks/useModuleActivityReport.js @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react'; +import { logError } from '@edx/frontend-platform/logging'; +import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService'; + +const useModuleActivityReport = ({ + enterpriseId, page, filters, searchQuery, +}) => { + const [isLoading, setIsLoading] = useState(true); + + const [paginationData, setPaginationData] = useState({ + itemCount: 0, + pageCount: 0, + data: [], + }); + + useEffect(() => { + // Reset the loading state + setIsLoading(true); + + EnterpriseDataApiService.fetchEnterpriseModuleActivityReport(enterpriseId, { + page: page + 1, + search: searchQuery, + ...filters, + }) + .then((response) => { + setPaginationData({ + itemCount: response.data.count, + pageCount: response.data.num_pages, + data: response.data.results, + currentPage: response.data.currentPage, + }); + + // Reset the loading state + setIsLoading(false); + }) + .catch((err) => { + logError(err); + + // Reset the loading state + setIsLoading(false); + }); + }, [ + enterpriseId, + page, + filters, + searchQuery, + ]); + + return { + isLoading, + paginationData, + }; +}; + +export default useModuleActivityReport; diff --git a/src/components/Admin/index.jsx b/src/components/Admin/index.jsx index d259d8f9a1..bb5b983067 100644 --- a/src/components/Admin/index.jsx +++ b/src/components/Admin/index.jsx @@ -15,7 +15,7 @@ import EnrollmentsTable from '../EnrollmentsTable'; import RegisteredLearnersTable from '../RegisteredLearnersTable'; import EnrolledLearnersTable from '../EnrolledLearnersTable'; import EnrolledLearnersForInactiveCoursesTable from '../EnrolledLearnersForInactiveCoursesTable'; -import CompletedLearnersTable from '../CompletedLearnersTable'; +import CompletedLearnersTable from './CompletedLearnersTable'; import PastWeekPassedLearnersTable from '../PastWeekPassedLearnersTable'; import LearnerActivityTable from '../LearnerActivityTable'; import DownloadCsvButton from '../../containers/DownloadCsvButton'; diff --git a/src/components/Admin/tabs/ModuleActivityReport.jsx b/src/components/Admin/tabs/ModuleActivityReport.jsx index 88cbe3fc37..753c9f9471 100644 --- a/src/components/Admin/tabs/ModuleActivityReport.jsx +++ b/src/components/Admin/tabs/ModuleActivityReport.jsx @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import EnterpriseDataApiService from '../../../data/services/EnterpriseDataApiService'; import DownloadCsvButton from './DownloadCSVButton'; -import useModuleActivityReport from '../data/hooks'; +import useModuleActivityReport from '../data/hooks/useModuleActivityReport'; import SearchBar from '../../SearchBar'; const ModuleActivityReport = ({ enterpriseId }) => { diff --git a/src/components/CompletedLearnersTable/__snapshots__/CompletedLearnersTable.test.jsx.snap b/src/components/CompletedLearnersTable/__snapshots__/CompletedLearnersTable.test.jsx.snap deleted file mode 100644 index 722c7b349f..0000000000 --- a/src/components/CompletedLearnersTable/__snapshots__/CompletedLearnersTable.test.jsx.snap +++ /dev/null @@ -1,37 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CompletedLearnersTable renders empty state correctly 1`] = ` - - - - - - - - - There are no results. - - - -`; diff --git a/src/components/CompletedLearnersTable/index.jsx b/src/components/CompletedLearnersTable/index.jsx deleted file mode 100644 index 87bcda6bdf..0000000000 --- a/src/components/CompletedLearnersTable/index.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; - -import { useIntl } from '@edx/frontend-platform/i18n'; - -import TableContainer from '../../containers/TableContainer'; -import EnterpriseDataApiService from '../../data/services/EnterpriseDataApiService'; - -const CompletedLearnersTable = () => { - const intl = useIntl(); - - const tableColumns = [ - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.completed.learners.table.user_email.column.heading', - defaultMessage: 'Email', - description: 'Column heading for the user email column in the completed learners table', - }), - key: 'user_email', - columnSortable: true, - }, - { - label: intl.formatMessage({ - id: 'admin.portal.lpr.completed.learned.table.completed_courses.column.heading', - defaultMessage: 'Total Course Completed Count', - description: 'Column heading for the completed courses column in the completed learners table', - }), - key: 'completed_courses', - columnSortable: true, - }, - ]; - - const formatLearnerData = learners => learners.map(learner => ({ - ...learner, - user_email: {learner.user_email}, - })); - - return ( - - ); -}; - -export default CompletedLearnersTable; 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;