-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e0288ab
commit 8e070c8
Showing
12 changed files
with
447 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
src/components/PeopleManagement/tests/CreateGroupModal.test.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.