diff --git a/src/components/PeopleManagement/DownloadCSVButton.jsx b/src/components/PeopleManagement/DownloadCSVButton.jsx
new file mode 100644
index 000000000..fff0a6613
--- /dev/null
+++ b/src/components/PeopleManagement/DownloadCSVButton.jsx
@@ -0,0 +1,113 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import {
+ Toast, StatefulButton, Icon, Spinner, useToggle,
+} from '@openedx/paragon';
+import { Download, Check } from '@openedx/paragon/icons';
+import { logError } from '@edx/frontend-platform/logging';
+import { downloadCsv } from '../../utils';
+
+const csvHeaders = ['Name', 'Email', 'Joined Organization', 'Enrollments'];
+
+const dataEntryToRow = (entry) => {
+ const { enterpriseCustomerUser: { name, email, joinedOrg }, enrollments } = entry;
+ return [name, email, joinedOrg, enrollments];
+};
+
+const getCsvFileName = () => {
+ const padTwoZeros = (num) => num.toString().padStart(2, '0');
+ const currentDate = new Date();
+ const year = currentDate.getUTCFullYear();
+ const month = padTwoZeros(currentDate.getUTCMonth() + 1);
+ const day = padTwoZeros(currentDate.getUTCDate());
+ return `${year}-${month}-${day}-people-report.csv`;
+};
+
+const DownloadCsvButton = ({ testId, fetchData, totalCt }) => {
+ const [buttonState, setButtonState] = useState('pageLoading');
+ const [isOpen, open, close] = useToggle(false);
+ const intl = useIntl();
+
+ useEffect(() => {
+ if (fetchData) {
+ setButtonState('default');
+ }
+ }, [fetchData]);
+
+ const handleClick = async () => {
+ setButtonState('pending');
+ fetchData().then((response) => {
+ downloadCsv(getCsvFileName(), response.results, csvHeaders, dataEntryToRow);
+ open();
+ setButtonState('complete');
+ }).catch((err) => {
+ logError(err);
+ });
+ };
+
+ const toastText = intl.formatMessage({
+ id: 'adminPortal.peopleManagement.dataTable.download.toast',
+ defaultMessage: 'Successfully downloaded',
+ description: 'Toast message for the people management download button.',
+ });
+ return (
+ <>
+ { isOpen
+ && (
+
+ {toastText}
+
+ )}
+ ,
+ pending: ,
+ complete: ,
+ pageLoading: ,
+ }}
+ disabledStates={['pending', 'pageLoading']}
+ onClick={handleClick}
+ />
+ >
+ );
+};
+
+DownloadCsvButton.defaultProps = {
+ testId: 'download-csv-button',
+};
+
+DownloadCsvButton.propTypes = {
+ // eslint-disable-next-line react/forbid-prop-types
+ fetchData: PropTypes.func.isRequired,
+ totalCt: PropTypes.number,
+ testId: PropTypes.string,
+};
+
+export default DownloadCsvButton;
diff --git a/src/components/PeopleManagement/PeopleManagementTable.jsx b/src/components/PeopleManagement/PeopleManagementTable.jsx
index 62d0d5745..3602e9b79 100644
--- a/src/components/PeopleManagement/PeopleManagementTable.jsx
+++ b/src/components/PeopleManagement/PeopleManagementTable.jsx
@@ -7,6 +7,7 @@ import TableTextFilter from '../learner-credit-management/TableTextFilter';
import CustomDataTableEmptyState from '../learner-credit-management/CustomDataTableEmptyState';
import OrgMemberCard from './OrgMemberCard';
import useEnterpriseMembersTableData from './data/hooks/useEnterpriseMembersTableData';
+import DownloadCsvButton from './DownloadCSVButton';
const FilterStatus = (rest) => ;
@@ -15,10 +16,10 @@ const PeopleManagementTable = ({ enterpriseId }) => {
isLoading: isTableLoading,
enterpriseMembersTableData,
fetchEnterpriseMembersTableData,
+ fetchAllEnterpriseMembersData,
} = useEnterpriseMembersTableData({ enterpriseId });
const tableColumns = [{ Header: 'Name', accessor: 'name' }];
-
return (
{
itemCount={enterpriseMembersTableData.itemCount}
pageCount={enterpriseMembersTableData.pageCount}
EmptyTableComponent={CustomDataTableEmptyState}
+ tableActions={[
+ ,
+ ]}
>
{
pageCount: 0,
results: [],
});
+
+ const fetchAllEnterpriseMembersData = useCallback(async () => {
+ const { options, itemCount } = enterpriseMembersTableData;
+ // Take the existing filters but specify we're taking all results on one page
+ const fetchAllOptions = { ...options, page: 1, page_size: itemCount };
+ const response = await LmsApiService.fetchEnterpriseCustomerMembers(enterpriseId, fetchAllOptions);
+ return camelCaseObject(response.data);
+ }, [enterpriseId, enterpriseMembersTableData]);
+
const fetchEnterpriseMembersData = useCallback((args) => {
const fetch = async () => {
try {
@@ -33,6 +42,7 @@ const useEnterpriseMembersTableData = ({ enterpriseId }) => {
itemCount: data.count,
pageCount: data.numPages ?? Math.floor(data.count / options.pageSize),
results: data.results,
+ options,
});
} catch (error) {
logError(error);
@@ -56,6 +66,7 @@ const useEnterpriseMembersTableData = ({ enterpriseId }) => {
isLoading,
enterpriseMembersTableData,
fetchEnterpriseMembersTableData: debouncedFetchEnterpriseMembersData,
+ fetchAllEnterpriseMembersData,
};
};
diff --git a/src/components/PeopleManagement/tests/DownloadCsvButton.test.jsx b/src/components/PeopleManagement/tests/DownloadCsvButton.test.jsx
new file mode 100644
index 000000000..16a33ef94
--- /dev/null
+++ b/src/components/PeopleManagement/tests/DownloadCsvButton.test.jsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { logError } from '@edx/frontend-platform/logging';
+import { act, render, screen } from '@testing-library/react';
+
+import '@testing-library/jest-dom/extend-expect';
+
+import userEvent from '@testing-library/user-event';
+import DownloadCsvButton from '../DownloadCSVButton';
+import { downloadCsv } from '../../../utils';
+
+jest.mock('file-saver', () => ({
+ ...jest.requireActual('file-saver'),
+ saveAs: jest.fn(),
+}));
+
+jest.mock('../../../utils', () => ({
+ downloadCsv: jest.fn(),
+}));
+
+jest.mock('@edx/frontend-platform/logging', () => ({
+ ...jest.requireActual('@edx/frontend-platform/logging'),
+ logError: jest.fn(),
+}));
+
+jest.useFakeTimers({ advanceTimers: true }).setSystemTime(new Date('2024-01-20'));
+
+const mockData = {
+ results: [
+ {
+ enterprise_customer_user: {
+ email: 'a@letter.com',
+ joined_org: 'Apr 07, 2024',
+ name: 'A',
+ },
+ enrollments: 3,
+ },
+ {
+ enterprise_customer_user: {
+ email: 'b@letter.com',
+ joined_org: 'Apr 08, 2024',
+ name: 'B',
+ },
+ enrollments: 4,
+ },
+ ],
+};
+
+const testId = 'test-id-1';
+const DEFAULT_PROPS = {
+ totalCt: mockData.results.length,
+ fetchData: jest.fn(() => Promise.resolve(mockData)),
+ testId,
+};
+
+const DownloadCSVButtonWrapper = props => (
+
+
+
+);
+
+describe('DownloadCSVButton', () => {
+ const flushPromises = () => new Promise(setImmediate);
+
+ it('renders download csv button correctly.', async () => {
+ render();
+ expect(screen.getByTestId(testId)).toBeInTheDocument();
+
+ // Validate button text
+ expect(screen.getByText('Download all (2)')).toBeInTheDocument();
+
+ // Click the download button.
+ screen.getByTestId(testId).click();
+ await flushPromises();
+
+ expect(DEFAULT_PROPS.fetchData).toHaveBeenCalled();
+ const expectedFileName = '2024-01-20-people-report.csv';
+ const expectedHeaders = ['Name', 'Email', 'Joined Organization', 'Enrollments'];
+ expect(downloadCsv).toHaveBeenCalledWith(expectedFileName, mockData.results, expectedHeaders, expect.any(Function));
+ });
+ it('download button should handle error returned by the API endpoint.', async () => {
+ const props = {
+ ...DEFAULT_PROPS,
+ fetchData: jest.fn(() => Promise.reject(new Error('Error fetching data'))),
+ };
+ render();
+ expect(screen.getByTestId(testId)).toBeInTheDocument();
+
+ act(() => {
+ // Click the download button.
+ userEvent.click(screen.getByTestId(testId));
+ });
+
+ await flushPromises();
+
+ expect(DEFAULT_PROPS.fetchData).toHaveBeenCalled();
+ expect(logError).toHaveBeenCalled();
+ });
+});
diff --git a/src/utils.js b/src/utils.js
index fd034b75c..857fe4b3a 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,4 +1,5 @@
import dayjs from 'dayjs';
+import { saveAs } from 'file-saver';
import camelCase from 'lodash/camelCase';
import snakeCase from 'lodash/snakeCase';
import isArray from 'lodash/isArray';
@@ -609,6 +610,33 @@ function isTodayBetweenDates({ startDate, endDate }) {
*/
const isFalsy = (value) => value == null || value === '';
+/**
+ * Transform data to csv format and save to file
+ *
+ * @param {string} fileName
+ * Name of the file to save to
+ * @param {Array