Skip to content

Commit

Permalink
Merge pull request #1241 from openedx/asheehan-edx/fixing-select-all-…
Browse files Browse the repository at this point in the history
…members-table

 fix: allowing members table to select all
  • Loading branch information
alex-sheehan-edx authored May 31, 2024
2 parents 670c7b4 + 67332c7 commit 23eddf8
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const mockAssignableSubsidyAccessPolicy = {
},
isAssignable: true,
subsidyUuid: 'mock-subsidy-uuid',
catalogUuid: 'mock-catalog-uuid',
};

export const mockAssignableSubsidyAccessPolicyWithNoUtilization = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessA
import { useBudgetId, useSubsidyAccessPolicy } from '../data';

const GroupMembersCsvDownloadTableAction = ({
isEntireTableSelected,
tableInstance,
}) => {
const selectedEmails = Object.keys(tableInstance.state.selectedRowIds);
Expand Down Expand Up @@ -55,7 +56,7 @@ const GroupMembersCsvDownloadTableAction = ({
EnterpriseAccessApiService.fetchSubsidyHydratedGroupMembersData(
subsidyAccessPolicyId,
options,
selectedEmails,
isEntireTableSelected ? null : selectedEmails,
).then(response => {
// download CSV
const blob = new Blob([response.data], {
Expand All @@ -69,6 +70,13 @@ const GroupMembersCsvDownloadTableAction = ({
});
};

let buttonSelectedNumber = 0;
if (selectedEmailCount > 0) {
buttonSelectedNumber = isEntireTableSelected ? `(${tableInstance.itemCount})` : `(${selectedEmailCount})`;
} else {
buttonSelectedNumber = `all (${tableInstance.itemCount})`;
}

return (
<>
<AlertModal
Expand Down Expand Up @@ -99,13 +107,14 @@ const GroupMembersCsvDownloadTableAction = ({
className="border rounded-0 border-dark-500"
disabled={tableInstance.itemCount === 0}
>
Download {selectedEmailCount > 0 ? `(${selectedEmailCount})` : `all (${tableInstance.itemCount})`}
Download {buttonSelectedNumber}
</Button>
</>
);
};

GroupMembersCsvDownloadTableAction.propTypes = {
isEntireTableSelected: PropTypes.bool,
tableInstance: PropTypes.shape({
itemCount: PropTypes.number,
state: PropTypes.shape({
Expand All @@ -131,6 +140,7 @@ GroupMembersCsvDownloadTableAction.defaultProps = {
itemCount: 0,
state: {},
},
isEntireTableSelected: false,
};

export default GroupMembersCsvDownloadTableAction;
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ const KabobMenu = ({
);
};

const selectColumn = {
id: 'selection',
Header: DataTable.ControlledSelectHeader,
Cell: DataTable.ControlledSelect,
disableSortBy: true,
};

const LearnerCreditGroupMembersTable = ({
isLoading,
tableData,
Expand All @@ -70,8 +77,10 @@ const LearnerCreditGroupMembersTable = ({
}) => (
<DataTable
isSortable
isSelectable
manualSortBy
isSelectable
SelectionStatusComponent={DataTable.ControlledSelectionStatus}
manualSelectColumn={selectColumn}
isPaginated
manualPagination
isFilterable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ const MemberRemoveModal = ({
const handleSubmit = useCallback(async () => {
setRequestState({ ...initialRequestState, loading: true });
const makeRequest = () => {
const userEmailsToRemove = usersToRemove.map((user) => user.original.memberDetails.userEmail);
const requestBody = snakeCaseObject({
learnerEmails: userEmailsToRemove,
catalogUuid: subsidyAccessPolicy.catalogUuid,
});
const baseRequestBody = { catalogUuid: subsidyAccessPolicy.catalogUuid };
let requestBody;
if (removeAllUsers) {
requestBody = snakeCaseObject({ remove_all: true, ...baseRequestBody });
} else {
const userEmailsToRemove = usersToRemove.map((user) => user.original.memberDetails.userEmail);
requestBody = snakeCaseObject({ learnerEmails: userEmailsToRemove, ...baseRequestBody });
}
return LmsApiService.removeEnterpriseLearnersFromGroup(groupUuid, requestBody);
};

try {
const response = await makeRequest();
setRequestState({ ...initialRequestState, success: true });
Expand All @@ -77,6 +79,7 @@ const MemberRemoveModal = ({
setRequestState,
groupUuid,
subsidyAccessPolicy,
removeAllUsers,
]);

const handleClose = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,16 @@ describe('<BudgetDetailPage />', () => {
sortBy: [{ desc: false, id: 'status' }],
}));

// TODO Sorting by enrollment count is currently not supported by the backend
// userEvent.click(screen.getByTestId('members-table-enrollments-column-header'));
// await waitFor(() => expect(mockFetchEnterpriseGroupMembersTableData).toHaveBeenCalledWith({
// filters: [],
// pageIndex: 0,
// pageSize: 10,
// sortBy: [{ desc: false, id: 'enrollmentCount' }],
// }));
userEvent.click(screen.getByTestId('members-table-enrollments-column-header'));
await waitFor(() => expect(mockFetchEnterpriseGroupMembersTableData).toHaveBeenCalledWith({
filters: [
{ id: 'memberDetails', value: 'foobar' },
{ id: 'status', value: true },
],
pageIndex: 0,
pageSize: 10,
sortBy: [{ desc: false, id: 'enrollmentCount' }],
}));
});
it('remove learner flow', async () => {
const initialState = {
Expand Down Expand Up @@ -459,6 +461,7 @@ describe('<BudgetDetailPage />', () => {
enterpriseSlug: 'test-enterprise-slug',
enterpriseAppPage: 'test-enterprise-page',
activeTabKey: 'members',
budgetId: mockAssignableSubsidyAccessPolicy.uuid,
});
useSubsidyAccessPolicy.mockReturnValue({
isInitialLoading: false,
Expand Down Expand Up @@ -525,6 +528,13 @@ describe('<BudgetDetailPage />', () => {
expect(mockRemoveSpy).toHaveBeenCalled();
await waitForElementToBeRemoved(() => screen.queryByText('Removing (2)'));
await waitFor(() => expect(screen.queryByText('2 members successfully removed')).toBeInTheDocument());

// Because there is only one page of data, and the whole page is selected,
// the request should be to remove the entire org
expect(LmsApiService.removeEnterpriseLearnersFromGroup).toHaveBeenCalledWith(
mockAssignableSubsidyAccessPolicy.groupAssociations[0],
{ remove_all: true, catalog_uuid: mockAssignableSubsidyAccessPolicy.catalogUuid },
);
});
it('remove learner flow with the kabob menu', async () => {
const initialState = {
Expand Down Expand Up @@ -716,7 +726,7 @@ describe('<BudgetDetailPage />', () => {
const mockGroupData = {
isLoading: false,
enterpriseGroupMembersTableData: {
itemCount: 1,
itemCount: 100,
pageCount: 1,
results: [{
memberDetails: { userEmail: '[email protected]', userName: 'ayy lmao' },
Expand Down Expand Up @@ -844,4 +854,113 @@ describe('<BudgetDetailPage />', () => {
screen.getByText('This member has been successfully removed and can not browse this budget\'s '
+ 'catalog and enroll using their member permissions.');
});
it('download learner flow for multiple selected pages of users', async () => {
// Setup
const initialState = {
portalConfiguration: {
...initialStoreState.portalConfiguration,
enterpriseFeatures: {
enterpriseGroupsV1: true,
},
},
};
useParams.mockReturnValue({
enterpriseSlug: 'test-enterprise-slug',
enterpriseAppPage: 'test-enterprise-page',
activeTabKey: 'members',
budgetId: mockAssignableSubsidyAccessPolicy.uuid,
});
useSubsidyAccessPolicy.mockReturnValue({
isInitialLoading: false,
data: mockAssignableSubsidyAccessPolicy,
});
useBudgetDetailActivityOverview.mockReturnValue({
isLoading: false,
data: mockEmptyStateBudgetDetailActivityOverview,
});
useBudgetRedemptions.mockReturnValue({
isLoading: false,
budgetRedemptions: mockEmptyBudgetRedemptions,
fetchBudgetRedemptions: jest.fn(),
});
useEnterpriseGroupLearners.mockReturnValue({
data: {
count: 100,
currentPage: 1,
next: null,
numPages: 4,
results: {
enterpriseGroupMembershipUuid: 'cde2e374-032f-4c08-8c0d-bf3205fa7c7e',
learnerId: 4382,
memberDetails: { userEmail: '[email protected]', userName: 'duke silver' },
},
},
});
useEnterpriseGroupMembersTableData.mockReturnValue({
isLoading: false,
enterpriseGroupMembersTableData: {
// Item count tells the table whether or not there are more records that are not displayed by results
itemCount: 100,
pageCount: 4,
results: [{
memberDetails: { userEmail: '[email protected]', userName: 'duke silver' },
status: 'pending',
recentAction: 'Pending: April 02, 2024',
enrollmentCount: 0,
},
{
memberDetails: { userEmail: '[email protected]', userName: 'tammy 2' },
status: 'pending',
recentAction: 'Pending: April 02, 2024',
enrollmentCount: 0,
}],
},
fetchEnterpriseGroupMembersTableData: jest.fn(),
});
EnterpriseAccessApiService.fetchSubsidyHydratedGroupMembersData.mockResolvedValue({ status: 200 });
const mockDownloadSpy = jest.spyOn(EnterpriseAccessApiService, 'fetchSubsidyHydratedGroupMembersData');

renderWithRouter(<BudgetDetailPageWrapper initialState={initialState} />);
await waitFor(() => expect(screen.queryByText('[email protected]')).toBeInTheDocument());

// Select all the records on the current page
const selectAllCheckbox = screen.queryAllByRole('checkbox')[0];
userEvent.click(selectAllCheckbox);

// Download the results
const downloadButton = screen.queryByText('Download (2)');
userEvent.click(downloadButton);
// Expect the mock to have been called once
expect(mockDownloadSpy).toHaveBeenCalledTimes(1);
// Expect the fetch members call to have been made with the emails of the records selected on the current page
expect(EnterpriseAccessApiService.fetchSubsidyHydratedGroupMembersData).toHaveBeenCalledWith(
mockAssignableSubsidyAccessPolicy.uuid,
{
format_csv: true,
traverse_pagination: true,
group_uuid: mockAssignableSubsidyAccessPolicy.groupAssociations[0],
sort_by: 'member_details',
},
['[email protected]', '[email protected]'],
);

// Value correlates to the itemCount coming from ``useEnterpriseGroupMembersTableData``
// Click the select all to apply the selection to all records passed the currently selected page
const selectAllButton = screen.queryByText('Select all 100');
userEvent.click(selectAllButton);
userEvent.click(downloadButton);
// Expect an additional call to the mock
expect(mockDownloadSpy).toHaveBeenCalledTimes(2);
// Expect the call to fetch member records to be made without any email specification, indicating a fetch of all
expect(EnterpriseAccessApiService.fetchSubsidyHydratedGroupMembersData).toHaveBeenCalledWith(
mockAssignableSubsidyAccessPolicy.uuid,
{
format_csv: true,
traverse_pagination: true,
group_uuid: mockAssignableSubsidyAccessPolicy.groupAssociations[0],
sort_by: 'member_details',
},
null,
);
});
});

0 comments on commit 23eddf8

Please sign in to comment.