Skip to content

Commit

Permalink
feat: adds create group modal
Browse files Browse the repository at this point in the history
  • Loading branch information
katrinan029 committed Oct 8, 2024
1 parent e0288ab commit 8e070c8
Show file tree
Hide file tree
Showing 12 changed files with 447 additions and 10 deletions.
131 changes: 131 additions & 0 deletions src/components/PeopleManagement/CreateGroupModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React, { useCallback, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { snakeCaseObject } from '@edx/frontend-platform/utils';
import {
ActionRow, Button, FullscreenModal, StatefulButton, useToggle,
} from '@openedx/paragon';
import LmsApiService from '../../data/services/LmsApiService';
import InviteModalContent from '../learner-credit-management/invite-modal/InviteModalContent';
import SystemErrorAlertModal from '../learner-credit-management/cards/assignment-allocation-status-modals/SystemErrorAlertModal';

const CreateGroupModal = ({
isModalOpen,
closeModal,
enterpriseUUID,
}) => {
const intl = useIntl();
const [learnerEmails, setLearnerEmails] = useState([]);
const [createButtonState, setCreateButtonState] = useState('default');
const [groupName, setGroupName] = useState('');
const [canCreateGroup, setCanCreateGroup] = useState(false);
const [canInviteMembers, setCanInviteMembers] = useState(false);
const [isSystemErrorModalOpen, openSystemErrorModal, closeSystemErrorModal] = useToggle(false);
const handleCloseCreateGroupModal = () => {
closeModal();
setCreateButtonState('default');
};

const handleCreateGroup = async () => {
setCreateButtonState('pending');
const options = {
enterpriseUUID,
groupName,
};
let groupCreationResponse;

try {
groupCreationResponse = await LmsApiService.createEnterpriseGroup(options);
} catch (err) {
setCreateButtonState('error');
openSystemErrorModal();
}

try {
if (groupCreationResponse.status === 201) {
const requestBody = snakeCaseObject({
learnerEmails,
});
await LmsApiService.inviteEnterpriseLearnersToGroup(groupCreationResponse.data.uuid, requestBody);
setCreateButtonState('complete');
handleCloseCreateGroupModal();
}
} catch {
setCreateButtonState('error');
openSystemErrorModal();
}
};

const handleEmailAddressesChange = useCallback((
value,
{ canInvite = false } = {},
) => {
setLearnerEmails(value);
setCanInviteMembers(canInvite);
}, []);

useEffect(() => {
setCanCreateGroup(false);
if (groupName.length > 0 && canInviteMembers) {
setCanCreateGroup(true);
}
}, [groupName, canInviteMembers]);

return (
<>
<FullscreenModal
className="stepper-modal bg-light-200"
isOpen={isModalOpen}
onClose={handleCloseCreateGroupModal}
title={intl.formatMessage({
id: 'peopleManagement.tab.create.group.modal.title',
defaultMessage: 'New group',
description: 'Title for creating a new group modal',
})}
footerNode={(
<ActionRow>
<ActionRow.Spacer />
<Button variant="tertiary" onClick={handleCloseCreateGroupModal}>Cancel</Button>
<StatefulButton
labels={{
default: 'Create',
pending: 'Creating...',
complete: 'Created',
error: 'Try again',
}}
variant="primary"
state={createButtonState}
disabled={!canCreateGroup}
onClick={handleCreateGroup}
/>
</ActionRow>
)}
>
<InviteModalContent
onSetGroupName={setGroupName}
onEmailAddressesChange={handleEmailAddressesChange}
isGroupInvite
/>
</FullscreenModal>
<SystemErrorAlertModal
isErrorModalOpen={isSystemErrorModalOpen}
closeErrorModal={closeSystemErrorModal}
closeAssignmentModal={handleCloseCreateGroupModal}
retry={handleCreateGroup}
/>
</>
);
};

const mapStateToProps = state => ({
enterpriseUUID: state.portalConfiguration.enterpriseId,
});

CreateGroupModal.propTypes = {
enterpriseUUID: PropTypes.string.isRequired,
isModalOpen: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
};

export default connect(mapStateToProps)(CreateGroupModal);
2 changes: 2 additions & 0 deletions src/components/PeopleManagement/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const MAX_LENGTH_GROUP_NAME = 60;
export default MAX_LENGTH_GROUP_NAME;
2 changes: 2 additions & 0 deletions src/components/PeopleManagement/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import cardImage from './images/ZeroStateImage.svg';
import Hero from '../Hero';
import { SUBSIDY_TYPES } from '../../data/constants/subsidyTypes';
import { EnterpriseSubsidiesContext } from '../EnterpriseSubsidiesContext';
import CreateGroupModal from './CreateGroupModal';

const PeopleManagementPage = () => {
const intl = useIntl();
Expand Down Expand Up @@ -69,6 +70,7 @@ const PeopleManagementPage = () => {
description="CTA button text to open new group modal."
/>
</Button>
<CreateGroupModal isModalOpen={isModalOpen} openModel={openModal} closeModal={closeModal} />
</ActionRow>
<Card>
<Card.ImageCap
Expand Down
132 changes: 132 additions & 0 deletions src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React from 'react';
import {
fireEvent, render, screen, waitFor,
} from '@testing-library/react';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/extend-expect';
import { QueryClientProvider } from '@tanstack/react-query';
import { IntlProvider } from '@edx/frontend-platform/i18n';
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';

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

const mockStore = configureMockStore([thunk]);
const getMockStore = store => mockStore(store);
const enterpriseSlug = 'test-enterprise';
const enterpriseUUID = '1234';
const initialStoreState = {
portalConfiguration: {
enterpriseId: enterpriseUUID,
enterpriseSlug,
enableLearnerPortal: true,
enterpriseFeatures: {
topDownAssignmentRealTimeLcm: true,
enterpriseGroupsV1: true,
},
},
};

const defaultProps = {
isModalOpen: true,
closeModal: jest.fn(),
enterpriseUUID: 'test-uuid',
};

const CreateGroupModalWrapper = ({
initialState = initialStoreState,
}) => {
const store = getMockStore({ ...initialState });
return (
<IntlProvider locale="en">
<Provider store={store}>
<QueryClientProvider client={queryClient()}>
<CreateGroupModal {...defaultProps} />
</QueryClientProvider>
</Provider>
</IntlProvider>
);
};

describe('<InviteMemberModal />', () => {
it('Modal renders as expected', async () => {
render(<CreateGroupModalWrapper />);
expect(screen.getByText('Create a custom group of members')).toBeInTheDocument();
expect(screen.getByText('Name your group')).toBeInTheDocument();
expect(screen.getByText('Select group members')).toBeInTheDocument();
expect(screen.getByText('Upload a CSV or select members from the table below.')).toBeInTheDocument();
expect(screen.getByText('You haven\'t uploaded any members yet.')).toBeInTheDocument();
expect(screen.getByText('Upload a CSV file or select members to get started.')).toBeInTheDocument();
expect(screen.getByText('Create')).toBeInTheDocument();
expect(screen.getByText('Cancel')).toBeInTheDocument();
});
it('creates groups and assigns learners', async () => {
const mockCreateGroup = jest.spyOn(LmsApiService, 'createEnterpriseGroup');
const mockInvite = jest.spyOn(LmsApiService, 'inviteEnterpriseLearnersToGroup');

const mockGroupData = { uuid: 'test-uuid' };
LmsApiService.createEnterpriseGroup.mockResolvedValue({ status: 201, data: mockGroupData });

const mockInviteData = { records_processed: 1, new_learners: 1, existing_learners: 0 };
LmsApiService.inviteEnterpriseLearnersToGroup.mockResolvedValue(mockInviteData);

render(<CreateGroupModalWrapper />);
expect(screen.getByText('You haven\'t uploaded any members yet.')).toBeInTheDocument();
expect(screen.getByText('Upload a CSV file or select members to get started.')).toBeInTheDocument();
const fakeFile = new File(['[email protected]'], 'emails.csv', { type: 'text/csv' });
const dropzone = screen.getByText('Drag and drop your file here or click to upload.');
Object.defineProperty(dropzone, 'files', {
value: [fakeFile],
});
fireEvent.drop(dropzone);

await waitFor(() => {
expect(screen.getByText('emails.csv')).toBeInTheDocument();
expect(screen.getByText('Summary (1)')).toBeInTheDocument();
expect(screen.getByText('Total members to invite')).toBeInTheDocument();
expect(screen.getByText('[email protected]')).toBeInTheDocument();
const formFeedbackText = 'Maximum invite at a time: 1000';
expect(screen.queryByText(formFeedbackText)).not.toBeInTheDocument();
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 });

const createButton = screen.getByRole('button', { name: 'Create' });
userEvent.click(createButton);
expect(mockCreateGroup).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(mockInvite).toHaveBeenCalledTimes(1);
});
});
it('displays system error modal', async () => {
const mockCreateGroup = jest.spyOn(LmsApiService, 'createEnterpriseGroup');
const mockInvite = jest.spyOn(LmsApiService, 'inviteEnterpriseLearnersToGroup');

mockCreateGroup.mockRejectedValue({
customAttributes: {
httpErrorStatus: 404,
},
});
mockInvite.mockRejectedValue({
customAttributes: {
httpErrorStatus: 404,
},
});
render(<CreateGroupModalWrapper />);
const groupNameInput = screen.getByTestId('group-name');
expect(groupNameInput).toBeInTheDocument();
userEvent.type(groupNameInput, 'test group name');
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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const InviteMembersModalWrapper = ({
<InviteModalContent
onEmailAddressesChange={handleEmailAddressesChanged}
subsidyAccessPolicy={subsidyAccessPolicy}
isGroupInvite={false}
/>
</FullscreenModal>
<SystemErrorAlertModal
Expand Down
Loading

0 comments on commit 8e070c8

Please sign in to comment.