Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding in remove capability for group members + csv download #1367

Merged
merged 15 commits into from
Jan 10, 2025
Merged
125 changes: 125 additions & 0 deletions src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx
Original file line number Diff line number Diff line change
@@ -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`;

Check warning on line 30 in src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx#L26-L30

Added lines #L26 - L30 were not covered by tests
};

const createCsvData = (jsonData) => jsonToCsv(jsonData.map(row => ({
kiram15 marked this conversation as resolved.
Show resolved Hide resolved
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], {

Check warning on line 45 in src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx#L42-L45

Added lines #L42 - L45 were not covered by tests
type: 'text/csv',
});
saveAs(blob, getCsvFileName());
open();

Check warning on line 49 in src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx#L48-L49

Added lines #L48 - L49 were not covered by tests
} catch {
openErrorModal();

Check warning on line 51 in src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx#L51

Added line #L51 was not covered by tests
} finally {
setButtonState('complete');

Check warning on line 53 in src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx#L53

Added line #L53 was not covered by tests
}
};

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
&& (
<Toast onClose={close} show={isOpen}>

Check warning on line 66 in src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/GroupDetailPage/DownloadCsvButton.jsx#L66

Added line #L66 was not covered by tests
{toastText}
</Toast>
)}
<GeneralErrorModal
isOpen={isErrorModalOpen}
close={closeErrorModal}
/>
<StatefulButton
state={buttonState}
className="download-button"
data-testid={testId}
labels={{
default: intl.formatMessage({
id: 'adminPortal.peopleManagement.groupDetail.downloadCsv.button',
defaultMessage: 'Download',
description: 'Label for the download button on the group detail page.',
}),
pending: intl.formatMessage({
id: 'adminPortal.peopleManagement.groupDetail.downloadCsv.button.pending',
defaultMessage: 'Downloading',
description: 'Label for the download button on the group detail page when the download is in progress.',
}),
complete: intl.formatMessage({
id: 'adminPortal.peopleManagement.groupDetail.downloadCsv.button.complete',
defaultMessage: 'Downloaded',
description: 'Label for the download button on the group detail page when the download is complete.',
}),
pageLoading: intl.formatMessage({
id: 'adminPortal.peopleManagement.groupDetail.downloadCsv.button.loading',
defaultMessage: 'Download',
description: 'Label for the download button on the group detail page when the page is loading.',
}),
}}
icons={{
default: <Icon src={Download} />,
pending: <Spinner animation="border" variant="light" size="sm" />,
complete: <Icon src={Check} />,
pageLoading: <Icon src={Download} variant="light" />,
}}
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;
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -146,6 +149,8 @@ const GroupDetailPage = () => {
tableData={enterpriseGroupLearnersTableData}
fetchTableData={fetchEnterpriseGroupLearnersTableData}
groupUuid={groupUuid}
refresh={refresh}
setRefresh={setRefresh}
/>
</div>
);
Expand Down
193 changes: 193 additions & 0 deletions src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx
Original file line number Diff line number Diff line change
@@ -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) => (
<DataTable.FilterStatus showFilteredFields={false} {...rest} />

Check warning on line 23 in src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx#L23

Added line #L23 was not covered by tests
);

const KabobMenu = ({
row, groupUuid, refresh, setRefresh,
}) => {
const [isRemoveModalOpen, openRemoveModal, closeRemoveModal] = useToggle(false);
const [isErrorModalOpen, openErrorModal, closeErrorModal] = useToggle(false);

return (
<>
<RemoveMemberModal
groupUuid={groupUuid}
row={row}
isOpen={isRemoveModalOpen}
close={closeRemoveModal}
openError={openErrorModal}
refresh={refresh}
setRefresh={setRefresh}
/>
<GeneralErrorModal
isOpen={isErrorModalOpen}
close={closeErrorModal}
/>
<Dropdown drop="top">
<Dropdown.Toggle
id="kabob-menu-dropdown"
data-testid="kabob-menu-dropdown"
as={IconButton}
src={MoreVert}
iconAs={Icon}
variant="primary"
/>
<Dropdown.Menu>
<Dropdown.Item onClick={openRemoveModal}>
<Icon src={RemoveCircle} className="mr-2 text-danger-500" />
<FormattedMessage
id="people.management.budgetDetail.membersTab.kabobMenu.removeMember"
defaultMessage="Remove member"
description="Remove member option in the kabob menu"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</>
);
};

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 (
<span className="budget-detail-assignments">
<DataTable
isSortable
manualSortBy
isSelectable
SelectionStatusComponent={DataTable.ControlledSelectionStatus}
manualSelectColumn={selectColumn}
isPaginated
manualPagination
isFilterable
manualFilters
isLoading={isLoading}
defaultColumnValues={{ Filter: TableTextFilter }}
FilterStatusComponent={FilterStatus}
numBreakoutFilters={2}
columns={[
{
Header: intl.formatMessage({
id: 'people.management.groups.detail.page.members.columns.memberDetails',
defaultMessage: 'Member details',
description:
'Column header for the Member details column in the People management Groups detail page',
}),
accessor: 'memberDetails',
Cell: MemberDetailsTableCell,
},
{
Header: intl.formatMessage({
id: 'people.management.groups.detail.page.members.columns.recentAction',
defaultMessage: 'Recent action',
description:
'Column header for the Recent action column in the People management Groups detail page',
}),
accessor: 'recentAction',
Cell: RecentActionTableCell,
disableFilters: true,
},
{
Header: EnrollmentsTableColumnHeader,
accessor: 'enrollmentCount',
Cell: ({ row }) => 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) => (
<KabobMenu
{...props}
groupUuid={groupUuid}
refresh={refresh}
setRefresh={setRefresh}
/>
),
},
]}
tableActions={[
<DownloadCsvButton
data={tableData.results}
testId="group-members-download"
/>,
]}
fetchData={fetchTableData}
data={tableData.results}
itemCount={tableData.itemCount}
pageCount={tableData.pageCount}
EmptyTableComponent={CustomDataTableEmptyState}
/>
</span>
);
};

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;
Loading
Loading