From 6367c41869470895136cfd035c81ba3a3220487b Mon Sep 17 00:00:00 2001 From: jajjibhai008 Date: Tue, 3 Dec 2024 15:57:01 +0500 Subject: [PATCH] feat: add paginators on reporting configurations page --- src/components/ReportingConfig/index.jsx | 99 +++++++--- src/components/ReportingConfig/index.test.jsx | 186 +++++++++++++++++- src/data/services/LmsApiService.js | 9 +- src/data/services/tests/LmsApiService.test.js | 2 +- 4 files changed, 270 insertions(+), 26 deletions(-) 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 40a388e1a6..9e5b2f99b9 100644 --- a/src/data/services/LmsApiService.js +++ b/src/data/services/LmsApiService.js @@ -119,8 +119,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: {