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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 => ({
Copy link
Contributor

@marlonkeating marlonkeating Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with generating the csv file like this is that it can only generate a file with the current page of group member results.

If you want to take the client side approach, you can do it (prior art here) but you'll need to do another fetch of the data with page_size set to the total count of the filtered results.

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