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
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, useIntl } from '@edx/frontend-platform/i18n';

import {
Icon, IconButtonWithTooltip, Toast, useToggle,
} from '@openedx/paragon';
import { Download } from '@openedx/paragon/icons';
import { logError } from '@edx/frontend-platform/logging';
import GeneralErrorModal from '../GeneralErrorModal';
import { downloadCsv, getTimeStampedFilename } from '../../../utils';

const csvHeaders = ['Name', 'Email', 'Recent action', 'Enrollments'];

const DownloadCsvIconButton = ({ fetchAllData, dataCount, testId }) => {
const [isToastOpen, openToast, closeToast] = useToggle(false);
const [isErrorModalOpen, openErrorModal, closeErrorModal] = useToggle(false);
const intl = useIntl();
const messages = defineMessages({
downloadToastText: {
id: 'adminPortal.peopleManagement.groupDetail.downloadCsv.toast',
defaultMessage: 'Downloaded group members',
description: 'Toast message for the download button on the group detail page.',
},
downloadHoverText: {
id: 'adminPortal.peopleManagement.groupDetail.downloadCsv.hoverTooltip',
defaultMessage: `Download (${dataCount})`,
description: 'Tooltip message for the download button on the group detail page.',
},
});

const dataEntryToRow = (entry) => {
const { memberDetails: { userEmail, userName }, recentAction, enrollments } = entry;
return [userName, userEmail, recentAction, enrollments];

Check warning on line 34 in src/components/PeopleManagement/GroupDetailPage/DownloadCsvIconButton.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/GroupDetailPage/DownloadCsvIconButton.jsx#L33-L34

Added lines #L33 - L34 were not covered by tests
};

const handleClick = async () => {
fetchAllData().then((response) => {
const fileName = getTimeStampedFilename('group-report.csv');
downloadCsv(fileName, response.results, csvHeaders, dataEntryToRow);
openToast();
}).catch((err) => {
logError(err);
openErrorModal();
});
};

return (
<>
{ isToastOpen
&& (
<Toast onClose={closeToast} show={isToastOpen}>
{intl.formatMessage(messages.downloadToastText)}
</Toast>
)}
<GeneralErrorModal
isOpen={isErrorModalOpen}
close={closeErrorModal}
/>
<IconButtonWithTooltip
data-testid={testId}
tooltipContent={intl.formatMessage(messages.downloadHoverText)}
src={Download}
iconAs={Icon}
alt="Download group members"
variant="primary"
onClick={handleClick}
/>
</>
);
};

DownloadCsvIconButton.defaultProps = {
testId: 'download-csv-icon-button',
};

DownloadCsvIconButton.propTypes = {
fetchAllData: PropTypes.func.isRequired,
dataCount: PropTypes.number.isRequired,
testId: PropTypes.string,
};

export default DownloadCsvIconButton;
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,14 @@ const GroupDetailPage = () => {
const [isEditModalOpen, openEditModal, closeEditModal] = useToggle(false);
const [isLoading, setIsLoading] = useState(true);
const [groupName, setGroupName] = useState(enterpriseGroup?.name);

const {
isLoading: isTableLoading,
enterpriseGroupLearnersTableData,
fetchEnterpriseGroupLearnersTableData,
fetchAllEnterpriseGroupLearnersData,
refresh,
setRefresh,
} = useEnterpriseGroupLearnersTableData({ groupUuid });
const handleNameUpdate = (name) => {
setGroupName(name);
Expand Down Expand Up @@ -106,7 +110,6 @@ const GroupDetailPage = () => {
<IconButtonWithTooltip
alt="icon to trash group"
key="trashGroupTooltip"
tooltipPlacement="top"
tooltipContent={tooltipContent}
src={Delete}
iconAs={Icon}
Expand Down Expand Up @@ -145,7 +148,11 @@ const GroupDetailPage = () => {
isLoading={isTableLoading}
tableData={enterpriseGroupLearnersTableData}
fetchTableData={fetchEnterpriseGroupLearnersTableData}
fetchAllData={fetchAllEnterpriseGroupLearnersData}
dataCount={enterpriseGroupLearnersTableData.itemCount}
groupUuid={groupUuid}
refresh={refresh}
setRefresh={setRefresh}
/>
</div>
);
Expand Down
197 changes: 197 additions & 0 deletions src/components/PeopleManagement/GroupDetailPage/GroupMembersTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
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 DownloadCsvIconButton from './DownloadCsvIconButton';
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,
fetchAllData,
dataCount,
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={[
<DownloadCsvIconButton
fetchAllData={fetchAllData}
dataCount={dataCount}
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,
fetchAllData: PropTypes.func.isRequired,
dataCount: PropTypes.number.isRequired,
groupUuid: PropTypes.string.isRequired,
refresh: PropTypes.bool.isRequired,
setRefresh: PropTypes.func.isRequired,
};

export default GroupMembersTable;
Loading
Loading