diff --git a/src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx b/src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx
new file mode 100644
index 000000000..20ef7d173
--- /dev/null
+++ b/src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx
@@ -0,0 +1,125 @@
+import React, { useState, useEffect } from 'react';
+import { saveAs } from 'file-saver';
+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 { jsonToCsv } from '../utils';
+import GeneralErrorModal from '../GeneralErrorModal';
+
+const DownloadCsvButton = ({ data, testId }) => {
+ const [buttonState, setButtonState] = useState('pageLoading');
+ const [isOpen, open, close] = useToggle(false);
+ const [isErrorModalOpen, openErrorModal, closeErrorModal] = useToggle(false);
+ const intl = useIntl();
+
+ useEffect(() => {
+ if (data && data.length) {
+ setButtonState('default');
+ }
+ }, [data]);
+
+ const getCsvFileName = () => {
+ const currentDate = new Date();
+ const year = currentDate.getUTCFullYear();
+ const month = currentDate.getUTCMonth() + 1;
+ const day = currentDate.getUTCDate();
+ return `${year}-${month}-${day}-group-detail-report.csv`;
+ };
+
+ const createCsvData = (jsonData) => jsonToCsv(jsonData.map(row => ({
+ Email: row.memberDetails.userEmail,
+ Username: row.memberDetails.userName,
+ Enrollments: row.enrollments,
+ // we have to strip out the comma so it doesn't mess up the csv parsing
+ 'Recent action': row.recent_action.replace(/,/g, ''),
+ })));
+
+ const handleClick = async () => {
+ setButtonState('pending');
+ try {
+ const csv = createCsvData(data);
+ const blob = new Blob([csv], {
+ type: 'text/csv',
+ });
+ saveAs(blob, getCsvFileName());
+ open();
+ } catch {
+ openErrorModal();
+ } finally {
+ setButtonState('complete');
+ }
+ };
+
+ const toastText = intl.formatMessage({
+ id: 'adminPortal.peopleManagement.groupDetail.downloadCsv.toast',
+ defaultMessage: 'Downloaded group members.',
+ description: 'Toast message for the download button on the group detail page.',
+ });
+ 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
+ data: PropTypes.arrayOf(
+ PropTypes.object,
+ ),
+ testId: PropTypes.string,
+};
+
+export default DownloadCsvButton;
diff --git a/src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx b/src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx
index 84a0f2d78..276274c60 100644
--- a/src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx
+++ b/src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx
@@ -10,8 +10,8 @@ import { useEnterpriseGroupLearnersTableData, useEnterpriseGroupUuid } from '../
import { ROUTE_NAMES } from '../../EnterpriseApp/data/constants';
import DeleteGroupModal from './DeleteGroupModal';
import EditGroupNameModal from './EditGroupNameModal';
-import formatDates from '../utils';
-import GroupMembersTable from '../GroupMembersTable';
+import { formatDates } from '../utils';
+import GroupMembersTable from './GroupMembersTable';
const GroupDetailPage = () => {
const intl = useIntl();
@@ -21,10 +21,13 @@ const GroupDetailPage = () => {
const [isEditModalOpen, openEditModal, closeEditModal] = useToggle(false);
const [isLoading, setIsLoading] = useState(true);
const [groupName, setGroupName] = useState(enterpriseGroup?.name);
+
const {
isLoading: isTableLoading,
enterpriseGroupLearnersTableData,
fetchEnterpriseGroupLearnersTableData,
+ refresh,
+ setRefresh,
} = useEnterpriseGroupLearnersTableData({ groupUuid });
const handleNameUpdate = (name) => {
setGroupName(name);
@@ -146,6 +149,8 @@ const GroupDetailPage = () => {
tableData={enterpriseGroupLearnersTableData}
fetchTableData={fetchEnterpriseGroupLearnersTableData}
groupUuid={groupUuid}
+ refresh={refresh}
+ setRefresh={setRefresh}
/>
);
diff --git a/src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx b/src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx
new file mode 100644
index 000000000..95cdcfea3
--- /dev/null
+++ b/src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx
@@ -0,0 +1,193 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ DataTable, Dropdown, Icon, IconButton, useToggle,
+} from '@openedx/paragon';
+import { MoreVert, RemoveCircle } from '@openedx/paragon/icons';
+import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
+
+import TableTextFilter from '../../learner-credit-management/TableTextFilter';
+import CustomDataTableEmptyState from '../../learner-credit-management/CustomDataTableEmptyState';
+import MemberDetailsTableCell from '../../learner-credit-management/members-tab/MemberDetailsTableCell';
+import EnrollmentsTableColumnHeader from '../EnrollmentsTableColumnHeader';
+import {
+ GROUP_MEMBERS_TABLE_DEFAULT_PAGE,
+ GROUP_MEMBERS_TABLE_PAGE_SIZE,
+} from '../constants';
+import RecentActionTableCell from '../RecentActionTableCell';
+import DownloadCsvButton from './DownloadCsvButton';
+import RemoveMemberModal from './RemoveMemberModal';
+import GeneralErrorModal from '../GeneralErrorModal';
+
+const FilterStatus = (rest) => (
+
+);
+
+const KabobMenu = ({
+ row, groupUuid, refresh, setRefresh,
+}) => {
+ const [isRemoveModalOpen, openRemoveModal, closeRemoveModal] = useToggle(false);
+ const [isErrorModalOpen, openErrorModal, closeErrorModal] = useToggle(false);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+KabobMenu.propTypes = {
+ row: PropTypes.shape({}).isRequired,
+ groupUuid: PropTypes.string.isRequired,
+ refresh: PropTypes.bool.isRequired,
+ setRefresh: PropTypes.func.isRequired,
+};
+
+const selectColumn = {
+ id: 'selection',
+ Header: DataTable.ControlledSelectHeader,
+ Cell: DataTable.ControlledSelect,
+ disableSortBy: true,
+};
+
+const GroupMembersTable = ({
+ isLoading,
+ tableData,
+ fetchTableData,
+ groupUuid,
+ refresh,
+ setRefresh,
+}) => {
+ const intl = useIntl();
+ return (
+
+ row.original.enrollments,
+ disableFilters: true,
+ },
+ ]}
+ initialTableOptions={{
+ getRowId: (row) => row?.memberDetails.userEmail,
+ autoResetPage: true,
+ }}
+ initialState={{
+ pageSize: GROUP_MEMBERS_TABLE_PAGE_SIZE,
+ pageIndex: GROUP_MEMBERS_TABLE_DEFAULT_PAGE,
+ sortBy: [{ id: 'memberDetails', desc: true }],
+ filters: [],
+ }}
+ additionalColumns={[
+ {
+ id: 'action',
+ Header: '',
+ // eslint-disable-next-line react/no-unstable-nested-components
+ Cell: (props) => (
+
+ ),
+ },
+ ]}
+ tableActions={[
+ ,
+ ]}
+ fetchData={fetchTableData}
+ data={tableData.results}
+ itemCount={tableData.itemCount}
+ pageCount={tableData.pageCount}
+ EmptyTableComponent={CustomDataTableEmptyState}
+ />
+
+ );
+};
+
+GroupMembersTable.propTypes = {
+ isLoading: PropTypes.bool.isRequired,
+ tableData: PropTypes.shape({
+ results: PropTypes.arrayOf(PropTypes.shape({})),
+ itemCount: PropTypes.number.isRequired,
+ pageCount: PropTypes.number.isRequired,
+ }).isRequired,
+ fetchTableData: PropTypes.func.isRequired,
+ groupUuid: PropTypes.string.isRequired,
+ refresh: PropTypes.bool.isRequired,
+ setRefresh: PropTypes.func.isRequired,
+};
+
+export default GroupMembersTable;
diff --git a/src/components/PeopleManagement/GroupDetailPage/RemoveMemberModal.jsx b/src/components/PeopleManagement/GroupDetailPage/RemoveMemberModal.jsx
new file mode 100644
index 000000000..0e0e9fe88
--- /dev/null
+++ b/src/components/PeopleManagement/GroupDetailPage/RemoveMemberModal.jsx
@@ -0,0 +1,88 @@
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import PropTypes from 'prop-types';
+import {
+ ActionRow, Button, ModalDialog,
+} from '@openedx/paragon';
+import { RemoveCircle } from '@openedx/paragon/icons';
+import { logError } from '@edx/frontend-platform/logging';
+
+import LmsApiService from '../../../data/services/LmsApiService';
+
+const RemoveMemberModal = ({
+ groupUuid, row, isOpen, close, openError, refresh, setRefresh,
+}) => {
+ const removeEnterpriseGroupMember = async () => {
+ try {
+ const rowEmail = row.id;
+ const formData = new FormData();
+ formData.append('learner_emails', rowEmail);
+ await LmsApiService.removeEnterpriseLearnersFromGroup(groupUuid, 'hello');
+ setRefresh(!refresh);
+ close();
+ } catch (error) {
+ close();
+ logError(error);
+ openError();
+ }
+ };
+ return (
+
+
+
+ Remove member?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Go back
+
+
+
+
+
+ );
+};
+
+RemoveMemberModal.propTypes = {
+ groupUuid: PropTypes.string.isRequired,
+ row: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ }).isRequired,
+ isOpen: PropTypes.bool.isRequired,
+ close: PropTypes.func.isRequired,
+ openError: PropTypes.func.isRequired,
+ refresh: PropTypes.bool.isRequired,
+ setRefresh: PropTypes.func.isRequired,
+};
+
+export default RemoveMemberModal;
diff --git a/src/components/PeopleManagement/GroupMembersTable.jsx b/src/components/PeopleManagement/GroupMembersTable.jsx
deleted file mode 100644
index be59163a0..000000000
--- a/src/components/PeopleManagement/GroupMembersTable.jsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {
- DataTable, Dropdown, Icon, IconButton,
-} from '@openedx/paragon';
-import { MoreVert, RemoveCircle } from '@openedx/paragon/icons';
-import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
-import TableTextFilter from '../learner-credit-management/TableTextFilter';
-import CustomDataTableEmptyState from '../learner-credit-management/CustomDataTableEmptyState';
-import MemberDetailsTableCell from '../learner-credit-management/members-tab/MemberDetailsTableCell';
-import EnrollmentsTableColumnHeader from './EnrollmentsTableColumnHeader';
-import { GROUP_MEMBERS_TABLE_DEFAULT_PAGE, GROUP_MEMBERS_TABLE_PAGE_SIZE } from './constants';
-import RecentActionTableCell from './RecentActionTableCell';
-
-const FilterStatus = (rest) => ;
-
-const KabobMenu = () => (
-
-
-
-
-
-
-
-
-
-);
-
-const selectColumn = {
- id: 'selection',
- Header: DataTable.ControlledSelectHeader,
- Cell: DataTable.ControlledSelect,
- disableSortBy: true,
-};
-
-const GroupMembersTable = ({
- isLoading,
- tableData,
- fetchTableData,
- groupUuid,
-}) => {
- const intl = useIntl();
- return (
-
- row.original.enrollments,
- disableFilters: true,
- },
- ]}
- initialTableOptions={{
- getRowId: row => row?.memberDetails.userEmail,
- autoResetPage: true,
- }}
- initialState={{
- pageSize: GROUP_MEMBERS_TABLE_PAGE_SIZE,
- pageIndex: GROUP_MEMBERS_TABLE_DEFAULT_PAGE,
- sortBy: [
- { id: 'memberDetails', desc: true },
- ],
- filters: [],
- }}
- additionalColumns={[
- {
- id: 'action',
- Header: '',
- // eslint-disable-next-line react/no-unstable-nested-components
- Cell: (props) => (
-
- ),
- },
- ]}
- fetchData={fetchTableData}
- data={tableData.results}
- itemCount={tableData.itemCount}
- pageCount={tableData.pageCount}
- EmptyTableComponent={CustomDataTableEmptyState}
- />
-
- );
-};
-
-GroupMembersTable.propTypes = {
- isLoading: PropTypes.bool.isRequired,
- tableData: PropTypes.shape({
- results: PropTypes.arrayOf(PropTypes.shape({
- })),
- itemCount: PropTypes.number.isRequired,
- pageCount: PropTypes.number.isRequired,
- }).isRequired,
- fetchTableData: PropTypes.func.isRequired,
- groupUuid: PropTypes.string.isRequired,
-};
-
-export default GroupMembersTable;
diff --git a/src/components/PeopleManagement/RecentActionTableCell.jsx b/src/components/PeopleManagement/RecentActionTableCell.jsx
index cbc2fb75b..f76d04152 100644
--- a/src/components/PeopleManagement/RecentActionTableCell.jsx
+++ b/src/components/PeopleManagement/RecentActionTableCell.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import formatDates from './utils';
+import { formatDates } from './utils';
const RecentActionTableCell = ({
row,
diff --git a/src/components/PeopleManagement/constants.js b/src/components/PeopleManagement/constants.js
index ef2d298c3..fa98c28a4 100644
--- a/src/components/PeopleManagement/constants.js
+++ b/src/components/PeopleManagement/constants.js
@@ -13,4 +13,5 @@ export const GROUP_MEMBERS_TABLE_DEFAULT_PAGE = 0; // `DataTable` uses zero-inde
export const peopleManagementQueryKeys = {
all: ['people-management'],
members: (enterpriseUuid) => [...peopleManagementQueryKeys.all, 'members', enterpriseUuid],
+ removeMember: (groupUuid) => [...peopleManagementQueryKeys.all, 'removeMember', groupUuid],
};
diff --git a/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js
index 2e5d6e926..7ae9113e9 100644
--- a/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js
+++ b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js
@@ -10,6 +10,7 @@ import LmsApiService from '../../../../data/services/LmsApiService';
const useEnterpriseGroupLearnersTableData = ({ groupUuid }) => {
const [isLoading, setIsLoading] = useState(true);
+ const [refresh, setRefresh] = useState(false);
const [enterpriseGroupLearnersTableData, setEnterpriseGroupLearnersTableData] = useState({
itemCount: 0,
pageCount: 0,
@@ -56,14 +57,16 @@ const useEnterpriseGroupLearnersTableData = ({ groupUuid }) => {
const debouncedFetchEnterpriseGroupLearnersData = useMemo(
() => debounce(fetchEnterpriseGroupLearnersData, 300),
- [fetchEnterpriseGroupLearnersData],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [fetchEnterpriseGroupLearnersData, refresh],
);
return {
isLoading,
enterpriseGroupLearnersTableData,
fetchEnterpriseGroupLearnersTableData: debouncedFetchEnterpriseGroupLearnersData,
+ refresh,
+ setRefresh,
};
};
-
export default useEnterpriseGroupLearnersTableData;
diff --git a/src/components/PeopleManagement/utils.js b/src/components/PeopleManagement/utils.js
index 141d82600..93819161c 100644
--- a/src/components/PeopleManagement/utils.js
+++ b/src/components/PeopleManagement/utils.js
@@ -6,7 +6,19 @@ import dayjs from 'dayjs';
* @param {string} timestamp unformatted date timestamp
* @returns Formatted date string for display.
*/
-export default function formatDates(timestamp) {
+export function formatDates(timestamp) {
const DATE_FORMAT = 'MMMM DD, YYYY';
return dayjs(timestamp).format(DATE_FORMAT);
}
+
+export function jsonToCsv(data) {
+ let csv = '';
+ const headers = Object.keys(data[0]);
+ csv += `${headers.join(',')}\n`;
+
+ data.forEach((row) => {
+ const rows = headers.map(header => row[header]).join(',');
+ csv += `${rows}\n`;
+ });
+ return csv;
+}