diff --git a/src/components/ReportingConfig/index.jsx b/src/components/ReportingConfig/index.jsx
index 5108504f0f..6eae65c493 100644
--- a/src/components/ReportingConfig/index.jsx
+++ b/src/components/ReportingConfig/index.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Collapsible, Icon } from '@openedx/paragon';
+import { Collapsible, Icon, Pagination } from '@openedx/paragon';
import { Check, Close } from '@openedx/paragon/icons';
import { camelCaseObject } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -12,6 +12,7 @@ import LoadingMessage from '../LoadingMessage';
import ErrorPage from '../ErrorPage';
const STATUS_FULFILLED = 'fulfilled';
+const DEFAULT_PAGE_SIZE = 10;
class ReportingConfig extends React.Component {
// eslint-disable-next-line react/state-in-constructor
@@ -33,7 +34,15 @@ class ReportingConfig extends React.Component {
LMSApiService.fetchReportingConfigTypes(this.props.enterpriseId),
])
.then((responses) => {
+ let totalPages = responses[0].status === STATUS_FULFILLED ? responses[0].value.data.num_pages : 1;
+ if (!totalPages) {
+ totalPages = 1;
+ }
+
this.setState({
+ totalPages,
+ currentPage: 1,
+ totalRecords: responses[0].status === STATUS_FULFILLED ? responses[0].value.data.count : 0,
reportingConfigs: responses[0].status === STATUS_FULFILLED ? responses[0].value.data.results : undefined,
availableCatalogs: responses[1].status === STATUS_FULFILLED ? responses[1].value.data.results : undefined,
reportingConfigTypes: responses[2].status === STATUS_FULFILLED ? responses[2].value.data : undefined,
@@ -52,17 +61,25 @@ class ReportingConfig extends React.Component {
* @param {FormData} formData
*/
createConfig = async (formData) => {
- // snake_case the data before sending it to the backend
- const transformedData = snakeCaseFormData(formData);
try {
- const response = await LMSApiService.postNewReportingConfig(transformedData);
- this.setState(prevState => ({
- reportingConfigs: [
- ...prevState.reportingConfigs,
- response.data,
- ],
- }));
+ // Transform data to snake_case format
+ const transformedData = snakeCaseFormData(formData);
+
+ // Post the new configuration to the backend
+ await LMSApiService.postNewReportingConfig(transformedData);
+
+ const { totalRecords, totalPages } = this.state;
+
+ // Determine the target page to navigate to
+ const shouldAddNewPage = totalRecords % DEFAULT_PAGE_SIZE === 0 && totalRecords !== 0;
+ const targetPage = shouldAddNewPage ? totalPages + 1 : totalPages;
+
+ // Navigate to the appropriate page
+ this.handlePageSelect(targetPage);
+
+ // Close the new config form
this.newConfigFormRef.current.close();
+
return undefined;
} catch (error) {
return error;
@@ -72,18 +89,17 @@ class ReportingConfig extends React.Component {
deleteConfig = async (uuid) => {
try {
await LMSApiService.deleteReportingConfig(uuid);
- const deletedIndex = this.state.reportingConfigs
- .findIndex(config => config.uuid === uuid);
-
- this.setState((state) => {
- // Copy the existing, needs to be updated, list of reporting configs
- const newReportingConfig = [...state.reportingConfigs];
- // Splice out the one that's being deleted
- newReportingConfig.splice(deletedIndex, 1);
- return {
- reportingConfigs: newReportingConfig,
- };
- });
+
+ const isLastPage = this.state.currentPage === this.state.totalPages;
+ const hasOneRecord = this.state.reportingConfigs.length === 1;
+ const isOnlyRecordOnLastPage = hasOneRecord && isLastPage;
+
+ if (isOnlyRecordOnLastPage && this.state.currentPage > 1) {
+ this.handlePageSelect(this.state.totalPages - 1);
+ } else {
+ this.handlePageSelect(this.state.currentPage);
+ }
+
return undefined;
} catch (error) {
return error;
@@ -111,6 +127,32 @@ class ReportingConfig extends React.Component {
}
};
+ /**
+ * Handles page select event and fetches the data for the selected page
+ * @param {number} page - The page number to fetch data for
+ */
+ handlePageSelect = async (page) => {
+ this.setState({
+ loading: true,
+ });
+
+ try {
+ const response = await LMSApiService.fetchReportingConfigs(this.props.enterpriseId, page);
+ this.setState({
+ totalPages: response.data.num_pages || 1,
+ totalRecords: response.data.count,
+ currentPage: page,
+ reportingConfigs: response.data.results,
+ loading: false,
+ });
+ } catch (error) {
+ this.setState({
+ loading: false,
+ error,
+ });
+ }
+ };
+
render() {
const {
reportingConfigs,
@@ -118,6 +160,8 @@ class ReportingConfig extends React.Component {
error,
availableCatalogs,
reportingConfigTypes,
+ currentPage,
+ totalPages,
} = this.state;
const { intl } = this.props;
if (loading) {
@@ -200,6 +244,17 @@ class ReportingConfig extends React.Component {
))}
+
+ {reportingConfigs && reportingConfigs.length > 0 && (
+
+ )}
+
({
fetchReportingConfigs: jest.fn().mockResolvedValue({
@@ -170,4 +203,155 @@ describe('', () => {
const afterClickingDeleteButton = wrapper.find('button[data-testid="deleteConfigButton"]');
expect(afterClickingDeleteButton.exists()).toBe(false);
});
+ it('handles fetchReportingConfigs failure gracefully after deleting a record', async () => {
+ // Mock fetchReportingConfigs to return a valid response once
+ LmsApiService.fetchReportingConfigs = jest.fn().mockResolvedValueOnce(mockConfigsData).mockRejectedValueOnce(new Error('Failed to fetch reporting configs'));
+
+ let wrapper;
+
+ await act(async () => {
+ wrapper = mount(
+
+
+ ,
+ );
+ });
+
+ const configUuidToDelete = 'test-config-uuid';
+ // Update the wrapper after the state changes
+ wrapper.setState({
+ loading: false,
+ });
+ wrapper.update();
+
+ // Find the collapsible component and set its "isOpen" prop to true
+ const collapsibleTrigger = wrapper.find('.collapsible-trigger').at(0);
+ await act(async () => { collapsibleTrigger.simulate('click'); });
+ wrapper.update();
+ // Find the delete button using its data-testid and simulate a click event
+ const deleteButton = wrapper.find('button[data-testid="deleteConfigButton"]');
+ expect(deleteButton.exists()).toBe(true); // Ensure the button exists
+ await act(async () => { deleteButton.simulate('click'); });
+ wrapper.update();
+
+ // Verify that the deleteConfig function was called with the correct UUID
+ expect(LmsApiService.deleteReportingConfig).toHaveBeenCalledWith(configUuidToDelete);
+
+ const afterClickingDeleteButton = wrapper.find('button[data-testid="deleteConfigButton"]');
+ expect(afterClickingDeleteButton.exists()).toBe(false);
+
+ // Check for error handling
+ const errorMessage = wrapper.find(ErrorPage); // Adjust selector based on your error display logic
+ expect(errorMessage.exists()).toBe(true);
+ });
+ it('should not render Pagination when reportingConfigs is empty', async () => {
+ LmsApiService.fetchReportingConfigs.mockResolvedValue({
+ data: {
+ results: [],
+ count: 0,
+ num_pages: 0,
+ },
+ });
+
+ let wrapper;
+ await act(async () => {
+ wrapper = mount(
+
+
+ ,
+ );
+ });
+
+ wrapper.update();
+
+ // Check that Pagination component is not rendered when no configs
+ const paginationComponent = wrapper.find(Pagination);
+ expect(paginationComponent.exists()).toBe(false);
+ });
+ it('should render Pagination when reportingConfigs has items', async () => {
+ let wrapper;
+
+ LmsApiService.fetchReportingConfigs.mockResolvedValue({
+ data: {
+ results: [{
+ enterpriseCustomerId: 'test-customer-uuid',
+ active: true,
+ delivery_method: 'email',
+ uuid: 'test-config-uuid',
+ }],
+ count: 1,
+ num_pages: 1,
+ },
+ });
+
+ await act(async () => {
+ wrapper = mount(
+
+
+ ,
+ );
+ });
+
+ wrapper.update();
+
+ // Check that Pagination component is rendered when configs exist
+ const paginationComponent = wrapper.find(Pagination);
+ expect(paginationComponent.exists()).toBe(true);
+ });
+ it('calls createConfig function and handles new configuration creation', async () => {
+ // Mock the necessary API service methods
+ LmsApiService.postNewReportingConfig = jest.fn().mockResolvedValue({
+ data: {
+ uuid: 'new-config-uuid',
+ active: true,
+ delivery_method: 'email',
+ data_type: 'progress_v3',
+ frequency: 'monthly',
+ },
+ });
+
+ let wrapper;
+ await act(async () => {
+ wrapper = mount(
+
+
+ ,
+ );
+ });
+
+ // Wait for initial loading to complete
+ await act(async () => {
+ wrapper.update();
+ });
+
+ // Find and click the "Add a reporting configuration" collapsible
+ const addConfigCollapsible = wrapper.find('div.collapsible-trigger').last();
+ await act(async () => {
+ addConfigCollapsible.simulate('click');
+ });
+ wrapper.update();
+
+ // Prepare mock form data
+ const mockFormData = new FormData();
+ mockFormData.append('delivery_method', 'email');
+ mockFormData.append('data_type', 'progress_v3');
+ mockFormData.append('frequency', 'monthly');
+
+ // Find the ReportingConfigForm within the new config collapsible
+ const reportingConfigForm = wrapper.find('ReportingConfigForm').last();
+
+ // Mock the form submission
+ await act(async () => {
+ reportingConfigForm.prop('createConfig')(mockFormData);
+ });
+
+ // Verify that the postNewReportingConfig method was called
+ expect(LmsApiService.postNewReportingConfig).toHaveBeenCalled();
+
+ // Verify that a new page was fetched after configuration creation
+ expect(LmsApiService.fetchReportingConfigs).toHaveBeenCalledWith(
+ defaultProps.enterpriseId,
+ expect.any(Number),
+ );
+ });
});
diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js
index 3ac99b3c89..c566be13d9 100644
--- a/src/data/services/LmsApiService.js
+++ b/src/data/services/LmsApiService.js
@@ -121,8 +121,13 @@ class LmsApiService {
return LmsApiService.apiClient().post(requestCodesUrl, postParams);
}
- static fetchReportingConfigs(uuid) {
- return LmsApiService.apiClient().get(`${LmsApiService.reportingConfigUrl}?enterprise_customer=${uuid}&page_size=100`);
+ static fetchReportingConfigs(uuid, pageNumber) {
+ let url = `${LmsApiService.reportingConfigUrl}?enterprise_customer=${uuid}`;
+ if (pageNumber) {
+ url += `&page=${pageNumber}`;
+ }
+
+ return LmsApiService.apiClient().get(url);
}
static fetchReportingConfigTypes(uuid) {
diff --git a/src/data/services/tests/LmsApiService.test.js b/src/data/services/tests/LmsApiService.test.js
index 67d86e4590..11be3234a9 100644
--- a/src/data/services/tests/LmsApiService.test.js
+++ b/src/data/services/tests/LmsApiService.test.js
@@ -126,7 +126,7 @@ describe('LmsApiService', () => {
}],
},
});
- const response = await LmsApiService.fetchReportingConfigs();
+ const response = await LmsApiService.fetchReportingConfigs('test-enterprise-customer', 1);
expect(response).toEqual({
status: 200,
data: {