Skip to content

Commit 1010d7d

Browse files
fix: create group support multiple csv files (#1419)
test: create group remove and re-add user from csv file
1 parent c801303 commit 1010d7d

File tree

4 files changed

+75
-24
lines changed

4 files changed

+75
-24
lines changed

src/components/PeopleManagement/CreateGroupModalContent.jsx

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React, {
2-
useCallback, useEffect, useMemo, useState,
2+
useCallback, useEffect, useState,
33
} from 'react';
44
import PropTypes from 'prop-types';
5-
import debounce from 'lodash.debounce';
5+
import _ from 'lodash';
66
import {
77
Col, Container, Form, Hyperlink, Row,
88
} from '@openedx/paragon';
@@ -11,10 +11,11 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
1111
import InviteModalSummary from '../learner-credit-management/invite-modal/InviteModalSummary';
1212
import InviteSummaryCount from '../learner-credit-management/invite-modal/InviteSummaryCount';
1313
import FileUpload from '../learner-credit-management/invite-modal/FileUpload';
14-
import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isInviteEmailAddressesInputValueValid } from '../learner-credit-management/cards/data';
14+
import { isInviteEmailAddressesInputValueValid } from '../learner-credit-management/cards/data';
1515
import { HELP_CENTER_URL, MAX_LENGTH_GROUP_NAME } from './constants';
1616
import EnterpriseCustomerUserDataTable from './EnterpriseCustomerUserDataTable';
1717
import { useEnterpriseLearners } from '../learner-credit-management/data';
18+
import { splitAndTrim } from '../../utils';
1819

1920
const CreateGroupModalContent = ({
2021
enterpriseUUID,
@@ -25,7 +26,6 @@ const CreateGroupModalContent = ({
2526
setIsCreateGroupListSelection,
2627
}) => {
2728
const [learnerEmails, setLearnerEmails] = useState([]);
28-
const [emailAddressesInputValue, setEmailAddressesInputValue] = useState('');
2929
const [memberInviteMetadata, setMemberInviteMetadata] = useState({
3030
isValidInput: null,
3131
lowerCasedEmails: [],
@@ -69,25 +69,12 @@ const CreateGroupModalContent = ({
6969
setLearnerEmails(prev => prev.filter((el) => !value.includes(el)));
7070
}, [onEmailAddressesChange]);
7171

72-
const handleEmailAddressesChanged = useCallback((value) => {
73-
if (!value) {
74-
setLearnerEmails([]);
75-
onEmailAddressesChange([]);
76-
return;
77-
}
78-
// handles csv upload value and formats emails into an array of strings
79-
const emails = value.split('\n').map((email) => email.trim()).filter((email) => email.length > 0);
80-
setLearnerEmails(emails);
81-
}, [onEmailAddressesChange]);
82-
83-
const debouncedHandleEmailAddressesChanged = useMemo(
84-
() => debounce(handleEmailAddressesChanged, EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY),
85-
[handleEmailAddressesChanged],
86-
);
87-
88-
useEffect(() => {
89-
debouncedHandleEmailAddressesChanged(emailAddressesInputValue);
90-
}, [emailAddressesInputValue, debouncedHandleEmailAddressesChanged]);
72+
const handleCsvUpload = useCallback((emails) => {
73+
// Merge new emails with old emails (removing duplicates)
74+
const allEmails = _.union(learnerEmails, splitAndTrim('\n', emails));
75+
setLearnerEmails(allEmails);
76+
setIsCreateGroupFileUpload(true);
77+
}, [learnerEmails, setIsCreateGroupFileUpload]);
9178

9279
// Validate the learner emails emails from user input whenever it changes
9380
useEffect(() => {
@@ -151,7 +138,7 @@ const CreateGroupModalContent = ({
151138
</p>
152139
<FileUpload
153140
memberInviteMetadata={memberInviteMetadata}
154-
setEmailAddressesInputValue={setEmailAddressesInputValue}
141+
setEmailAddressesInputValue={handleCsvUpload}
155142
setIsCreateGroupFileUpload={setIsCreateGroupFileUpload}
156143
/>
157144
</Col>

src/components/PeopleManagement/tests/CreateGroupModal.test.jsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ const CreateGroupModalWrapper = ({
129129

130130
describe('<CreateGroupModal />', () => {
131131
beforeEach(() => {
132+
jest.clearAllMocks();
132133
useEnterpriseMembersTableData.mockReturnValue({
133134
isLoading: false,
134135
enterpriseMembersTableData: mockTabledata,
@@ -299,6 +300,54 @@ describe('<CreateGroupModal />', () => {
299300
});
300301
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['learner-credit-management', 'group', '1234'] });
301302
});
303+
it('removes and re-adds user from csv file', async () => {
304+
const mockGroupData = { uuid: 'test-uuid' };
305+
LmsApiService.createEnterpriseGroup.mockResolvedValue({ status: 201, data: mockGroupData });
306+
307+
const mockInviteData = { records_processed: 1, new_learners: 1, existing_learners: 0 };
308+
LmsApiService.inviteEnterpriseLearnersToGroup.mockResolvedValue(mockInviteData);
309+
310+
render(<CreateGroupModalWrapper />);
311+
const fakeFile = new File(['[email protected]\[email protected]'], 'emails.csv', { type: 'text/csv' });
312+
const dropzone = screen.getByText('Drag and drop your file here or click to upload.');
313+
Object.defineProperty(dropzone, 'files', {
314+
value: [fakeFile],
315+
writable: true,
316+
});
317+
fireEvent.drop(dropzone);
318+
319+
await waitFor(() => {
320+
expect(screen.getByText('Summary (2)')).toBeInTheDocument();
321+
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
322+
323+
// testing interaction with removing members from the datatable
324+
const membersCheckboxes = screen.getAllByRole('checkbox');
325+
// skipping first one because it's the select all checkbox
326+
userEvent.click(membersCheckboxes[1]);
327+
const removeMembersButton = screen.getByText('Remove');
328+
userEvent.click(removeMembersButton);
329+
330+
await waitFor(() => {
331+
expect(screen.getByText('Summary (1)')).toBeInTheDocument();
332+
expect(screen.getAllByText('[email protected]')).toHaveLength(1);
333+
expect(screen.getAllByText('[email protected]')).toHaveLength(2);
334+
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
335+
336+
const dropzone2 = screen.getByText('emails.csv');
337+
Object.defineProperty(dropzone2, 'files', {
338+
value: [fakeFile],
339+
});
340+
fireEvent.drop(dropzone2);
341+
342+
const groupNameInput = screen.getByTestId('group-name');
343+
userEvent.type(groupNameInput, 'test group name');
344+
345+
await waitFor(() => {
346+
expect(screen.getByText('Summary (2)')).toBeInTheDocument();
347+
expect(screen.getAllByText('[email protected]')).toHaveLength(2);
348+
expect(screen.getAllByText('[email protected]')).toHaveLength(2);
349+
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
350+
});
302351
it('displays error for email not belonging in an org', async () => {
303352
const mockGroupData = { uuid: 'test-uuid' };
304353
LmsApiService.createEnterpriseGroup.mockResolvedValue({ status: 201, data: mockGroupData });

src/utils.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,13 @@ function downloadCsv(fileName, data, headers, dataEntryToRow) {
655655
saveAs(blob, fileName);
656656
}
657657

658+
/**
659+
* Split a string by given separator, and return array of trimmed, non-blank string entries
660+
*/
661+
function splitAndTrim(separator, str) {
662+
return str.split(separator).map((subStr) => subStr.trim()).filter((subStr) => subStr.length > 0);
663+
}
664+
658665
export {
659666
camelCaseDict,
660667
camelCaseDictArray,
@@ -704,4 +711,5 @@ export {
704711
isFalsy,
705712
getTimeStampedFilename,
706713
downloadCsv,
714+
splitAndTrim,
707715
};

src/utils.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
i18nFormatProgressStatus,
1717
getTimeStampedFilename,
1818
downloadCsv,
19+
splitAndTrim,
1920
} from './utils';
2021

2122
jest.mock('@edx/frontend-platform/logging', () => ({
@@ -211,4 +212,10 @@ describe('utils', () => {
211212
expect(saveAs).toHaveBeenCalledWith({}, fileName);
212213
});
213214
});
215+
describe('splitAndTrim', () => {
216+
it('returns split and trimmed string array', () => {
217+
const csvStr = 'a,b,,c ,';
218+
expect(splitAndTrim(',', csvStr)).toEqual(['a', 'b', 'c']);
219+
});
220+
});
214221
});

0 commit comments

Comments
 (0)