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

feat: add enterprise users datatable #1341

Merged
merged 7 commits into from
Nov 12, 2024
Merged
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
24 changes: 24 additions & 0 deletions src/components/PeopleManagement/CreateGroupModalContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import FileUpload from '../learner-credit-management/invite-modal/FileUpload';
import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isInviteEmailAddressesInputValueValid } from '../learner-credit-management/cards/data';
import { MAX_LENGTH_GROUP_NAME } from './constants';
import EnterpriseCustomerUserDatatable from '../learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable';

const CreateGroupModalContent = ({
onEmailAddressesChange,
Expand Down Expand Up @@ -43,6 +44,24 @@
onSetGroupName(e.target.value);
}, [onSetGroupName]);

const handleAddMembersBulkAction = useCallback((value) => {
if (!value) {
setLearnerEmails([]);
onEmailAddressesChange([]);
return;

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

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/CreateGroupModalContent.jsx#L49-L51

Added lines #L49 - L51 were not covered by tests
}
setLearnerEmails(prev => [...prev, ...value]);
}, [onEmailAddressesChange]);

const handleRemoveMembersBulkAction = useCallback((value) => {
if (!value) {
setLearnerEmails([]);
onEmailAddressesChange([]);
return;

Check warning on line 60 in src/components/PeopleManagement/CreateGroupModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/CreateGroupModalContent.jsx#L58-L60

Added lines #L58 - L60 were not covered by tests
}
setLearnerEmails(prev => prev.filter((el) => !value.includes(el)));
}, [onEmailAddressesChange]);

const handleEmailAddressesChanged = useCallback((value) => {
if (!value) {
setLearnerEmails([]);
Expand Down Expand Up @@ -123,6 +142,11 @@
<hr className="my-4" />
</Col>
</Row>
<EnterpriseCustomerUserDatatable
onHandleAddMembersBulkAction={handleAddMembersBulkAction}
onHandleRemoveMembersBulkAction={handleRemoveMembersBulkAction}
learnerEmails={learnerEmails}
/>
</Container>
);
};
Expand Down
102 changes: 100 additions & 2 deletions src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,21 @@ import { queryClient } from '../../test/testUtils';
import LmsApiService from '../../../data/services/LmsApiService';
import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY } from '../../learner-credit-management/cards/data';
import CreateGroupModal from '../CreateGroupModal';
import {
useEnterpriseLearnersTableData,
useGetAllEnterpriseLearnerEmails,
} from '../../learner-credit-management/data/hooks/useEnterpriseLearnersTableData';

jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(),
}));
jest.mock('../../../data/services/LmsApiService');
jest.mock('../../learner-credit-management/data/hooks/useEnterpriseLearnersTableData', () => ({
...jest.requireActual('../../learner-credit-management/data/hooks/useEnterpriseLearnersTableData'),
useEnterpriseLearnersTableData: jest.fn(),
useGetAllEnterpriseLearnerEmails: jest.fn(),
}));

const mockStore = configureMockStore([thunk]);
const getMockStore = store => mockStore(store);
Expand All @@ -43,6 +52,45 @@ const defaultProps = {
enterpriseUUID: 'test-uuid',
};

const mockTabledata = {
itemCount: 3,
pageCount: 1,
results: [
{
id: 1,
user: {
id: 1,
username: 'testuser-1',
firstName: '',
lastName: '',
email: 'testuser-1@2u.com',
dateJoined: '2023-05-09T16:18:22Z',
},
},
{
id: 2,
user: {
id: 2,
username: 'testuser-2',
firstName: '',
lastName: '',
email: 'testuser-2@2u.com',
dateJoined: '2023-05-09T16:18:22Z',
},
},
{
id: 3,
user: {
id: 3,
username: 'testuser-3',
firstName: '',
lastName: '',
email: 'testuser-3@2u.com',
dateJoined: '2023-05-09T16:18:22Z',
},
},
],
};
const CreateGroupModalWrapper = ({
initialState = initialStoreState,
}) => {
Expand All @@ -59,6 +107,18 @@ const CreateGroupModalWrapper = ({
};

describe('<CreateGroupModal />', () => {
beforeEach(() => {
useEnterpriseLearnersTableData.mockReturnValue({
isLoading: false,
enterpriseCustomerUserTableData: mockTabledata,
fetchEnterpriseLearnersData: jest.fn(),
});
useGetAllEnterpriseLearnerEmails.mockReturnValue({
isLoading: false,
fetchLearnerEmails: jest.fn(),
addButtonState: 'complete',
});
});
it('Modal renders as expected', async () => {
render(<CreateGroupModalWrapper />);
expect(screen.getByText('Create a custom group of members')).toBeInTheDocument();
Expand All @@ -69,6 +129,16 @@ describe('<CreateGroupModal />', () => {
expect(screen.getByText('Upload a CSV file or select members to get started.')).toBeInTheDocument();
expect(screen.getByText('Create')).toBeInTheDocument();
expect(screen.getByText('Cancel')).toBeInTheDocument();

// renders datatable
expect(screen.getByText('Member details')).toBeInTheDocument();
expect(screen.getByText('Joined organization')).toBeInTheDocument();
expect(screen.getByText('testuser-1')).toBeInTheDocument();
expect(screen.getByText('testuser-1@2u.com')).toBeInTheDocument();
expect(screen.getByText('testuser-2')).toBeInTheDocument();
expect(screen.getByText('testuser-2@2u.com')).toBeInTheDocument();
expect(screen.getByText('testuser-3')).toBeInTheDocument();
expect(screen.getByText('testuser-3@2u.com')).toBeInTheDocument();
});
it('creates groups and assigns learners', async () => {
const mockCreateGroup = jest.spyOn(LmsApiService, 'createEnterpriseGroup');
Expand All @@ -93,10 +163,36 @@ describe('<CreateGroupModal />', () => {
userEvent.type(groupNameInput, 'test group name');

await waitFor(() => {
expect(screen.getByText('emails.csv')).toBeInTheDocument();
expect(screen.getByText('Summary (1)')).toBeInTheDocument();
expect(screen.getByText('tomhaverford@pawnee.org')).toBeInTheDocument();
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });

// testing interaction with adding members from the datatable
const membersCheckbox = screen.getAllByTitle('Toggle Row Selected');
userEvent.click(membersCheckbox[0]);
userEvent.click(membersCheckbox[1]);
const addMembersButton = screen.getByText('Add');
userEvent.click(addMembersButton);

await waitFor(() => {
expect(screen.getByText('Summary (3)')).toBeInTheDocument();
// checking that each user appears twice, once in the datatable and once in the summary section
expect(screen.getAllByText('testuser-1@2u.com')).toHaveLength(2);
expect(screen.getAllByText('testuser-2@2u.com')).toHaveLength(2);
});

// testing interaction with removing members from the datatable
const removeMembersButton = screen.getByText('Remove');
userEvent.click(removeMembersButton);

await waitFor(() => {
expect(screen.getByText('Summary (1)')).toBeInTheDocument();
expect(screen.getByText('emails.csv')).toBeInTheDocument();
expect(screen.getByText('Total members to add')).toBeInTheDocument();
expect(screen.getByText('tomhaverford@pawnee.org')).toBeInTheDocument();
expect(screen.getAllByText('testuser-1@2u.com')).toHaveLength(1);
expect(screen.getAllByText('testuser-2@2u.com')).toHaveLength(1);
expect(screen.getAllByText('testuser-3@2u.com')).toHaveLength(1);
const formFeedbackText = 'Maximum members at a time: 1000';
expect(screen.queryByText(formFeedbackText)).not.toBeInTheDocument();
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });
Expand Down Expand Up @@ -137,7 +233,9 @@ describe('<CreateGroupModal />', () => {
const createButton = screen.getByRole('button', { name: 'Create' });
userEvent.click(createButton);
await waitFor(() => {
expect(screen.getByText('We\'re sorry. Something went wrong behind the scenes. Please try again, or reach out to customer support for help.')).toBeInTheDocument();
expect(screen.getByText(
'We\'re sorry. Something went wrong behind the scenes. Please try again, or reach out to customer support for help.',
)).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
useCallback, useMemo, useState,
} from 'react';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { logError } from '@edx/frontend-platform/logging';
import debounce from 'lodash.debounce';

import LmsApiService from '../../../../data/services/LmsApiService';
import { fetchPaginatedData } from '../../../../data/services/apiServiceUtils';

export const useGetAllEnterpriseLearnerEmails = ({
enterpriseId,
onHandleAddMembersBulkAction,
}) => {
const [isLoading, setIsLoading] = useState(true);
const [addButtonState, setAddButtonState] = useState('default');

Check warning on line 16 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L14-L16

Added lines #L14 - L16 were not covered by tests

const fetchLearnerEmails = useCallback(async () => {
setAddButtonState('pending');
try {
const url = `${LmsApiService.enterpriseLearnerUrl}?enterprise_customer=${enterpriseId}`;
const { results } = await fetchPaginatedData(url);
const learnerEmails = results.map(result => result?.user?.email).filter(email => email !== undefined);
onHandleAddMembersBulkAction(learnerEmails);

Check warning on line 24 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L18-L24

Added lines #L18 - L24 were not covered by tests
} catch (error) {
logError(error);
setAddButtonState('error');

Check warning on line 27 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L26-L27

Added lines #L26 - L27 were not covered by tests
} finally {
setIsLoading(false);
setAddButtonState('complete');

Check warning on line 30 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L29-L30

Added lines #L29 - L30 were not covered by tests
}
}, [enterpriseId, onHandleAddMembersBulkAction]);

return {

Check warning on line 34 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L34

Added line #L34 was not covered by tests
isLoading,
fetchLearnerEmails,
addButtonState,
};
};

export const useEnterpriseLearnersTableData = (enterpriseId) => {
const [isLoading, setIsLoading] = useState(true);
const [enterpriseCustomerUserTableData, setEnterpriseCustomerUserTableData] = useState({

Check warning on line 43 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L42-L43

Added lines #L42 - L43 were not covered by tests
itemCount: 0,
pageCount: 0,
results: [],
});
const fetchEnterpriseLearnersData = useCallback(async (args) => {
try {
setIsLoading(true);
const options = {

Check warning on line 51 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L48-L51

Added lines #L48 - L51 were not covered by tests
enterprise_customer: enterpriseId,
};
options.page = args.pageIndex + 1;
const response = await LmsApiService.fetchEnterpriseLearners(options);
const { data } = camelCaseObject(response);
setEnterpriseCustomerUserTableData({

Check warning on line 57 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L54-L57

Added lines #L54 - L57 were not covered by tests
itemCount: data.count,
pageCount: data.numPages ?? Math.floor(data.count / options.pageSize),

Check warning on line 59 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L59

Added line #L59 was not covered by tests
results: data.results,
});
} catch (error) {
logError(error);

Check warning on line 63 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L63

Added line #L63 was not covered by tests
} finally {
setIsLoading(false);

Check warning on line 65 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L65

Added line #L65 was not covered by tests
}
}, [enterpriseId, setEnterpriseCustomerUserTableData]);

const debouncedFetchEnterpriseLearnersData = useMemo(
() => debounce(fetchEnterpriseLearnersData, 300),

Check warning on line 70 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L69-L70

Added lines #L69 - L70 were not covered by tests
[fetchEnterpriseLearnersData],
);

return {

Check warning on line 74 in src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useEnterpriseLearnersTableData.js#L74

Added line #L74 was not covered by tests
isLoading,
enterpriseCustomerUserTableData,
fetchEnterpriseLearnersData: debouncedFetchEnterpriseLearnersData,
};
};
Loading