Skip to content

Commit

Permalink
feat: add enterprise users datatable (#1341)
Browse files Browse the repository at this point in the history
* feat: add enterprise users datatable
  • Loading branch information
katrinan029 authored Nov 12, 2024
1 parent 0f957c5 commit ba8fc56
Show file tree
Hide file tree
Showing 7 changed files with 450 additions and 7 deletions.
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 InviteSummaryCount from '../learner-credit-management/invite-modal/Invite
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 @@ const CreateGroupModalContent = ({
onSetGroupName(e.target.value);
}, [onSetGroupName]);

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

const handleRemoveMembersBulkAction = useCallback((value) => {
if (!value) {
setLearnerEmails([]);
onEmailAddressesChange([]);
return;
}
setLearnerEmails(prev => prev.filter((el) => !value.includes(el)));
}, [onEmailAddressesChange]);

const handleEmailAddressesChanged = useCallback((value) => {
if (!value) {
setLearnerEmails([]);
Expand Down Expand Up @@ -123,6 +142,11 @@ const CreateGroupModalContent = ({
<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: '[email protected]',
dateJoined: '2023-05-09T16:18:22Z',
},
},
{
id: 2,
user: {
id: 2,
username: 'testuser-2',
firstName: '',
lastName: '',
email: '[email protected]',
dateJoined: '2023-05-09T16:18:22Z',
},
},
{
id: 3,
user: {
id: 3,
username: 'testuser-3',
firstName: '',
lastName: '',
email: '[email protected]',
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('[email protected]')).toBeInTheDocument();
expect(screen.getByText('testuser-2')).toBeInTheDocument();
expect(screen.getByText('[email protected]')).toBeInTheDocument();
expect(screen.getByText('testuser-3')).toBeInTheDocument();
expect(screen.getByText('[email protected]')).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('[email protected]')).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('[email protected]')).toHaveLength(2);
expect(screen.getAllByText('[email protected]')).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('[email protected]')).toBeInTheDocument();
expect(screen.getAllByText('[email protected]')).toHaveLength(1);
expect(screen.getAllByText('[email protected]')).toHaveLength(1);
expect(screen.getAllByText('[email protected]')).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');

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);
} catch (error) {
logError(error);
setAddButtonState('error');
} finally {
setIsLoading(false);
setAddButtonState('complete');
}
}, [enterpriseId, onHandleAddMembersBulkAction]);

return {
isLoading,
fetchLearnerEmails,
addButtonState,
};
};

export const useEnterpriseLearnersTableData = (enterpriseId) => {
const [isLoading, setIsLoading] = useState(true);
const [enterpriseCustomerUserTableData, setEnterpriseCustomerUserTableData] = useState({
itemCount: 0,
pageCount: 0,
results: [],
});
const fetchEnterpriseLearnersData = useCallback(async (args) => {
try {
setIsLoading(true);
const options = {
enterprise_customer: enterpriseId,
};
options.page = args.pageIndex + 1;
const response = await LmsApiService.fetchEnterpriseLearners(options);
const { data } = camelCaseObject(response);
setEnterpriseCustomerUserTableData({
itemCount: data.count,
pageCount: data.numPages ?? Math.floor(data.count / options.pageSize),
results: data.results,
});
} catch (error) {
logError(error);
} finally {
setIsLoading(false);
}
}, [enterpriseId, setEnterpriseCustomerUserTableData]);

const debouncedFetchEnterpriseLearnersData = useMemo(
() => debounce(fetchEnterpriseLearnersData, 300),
[fetchEnterpriseLearnersData],
);

return {
isLoading,
enterpriseCustomerUserTableData,
fetchEnterpriseLearnersData: debouncedFetchEnterpriseLearnersData,
};
};
Loading

0 comments on commit ba8fc56

Please sign in to comment.