-
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.
feat: adds create group modal (#1330)
- Loading branch information
1 parent
912940c
commit 06c0a56
Showing
13 changed files
with
460 additions
and
12 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,132 @@ | ||
import React, { useCallback, useState, useEffect } from 'react'; | ||
import { logError } from '@edx/frontend-platform/logging'; | ||
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) { | ||
logError(err); | ||
setCreateButtonState('error'); | ||
openSystemErrorModal(); | ||
} | ||
|
||
try { | ||
const requestBody = snakeCaseObject({ | ||
learnerEmails, | ||
}); | ||
await LmsApiService.inviteEnterpriseLearnersToGroup(groupCreationResponse.data.uuid, requestBody); | ||
setCreateButtonState('complete'); | ||
handleCloseCreateGroupModal(); | ||
} catch (err) { | ||
logError(err); | ||
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
142 changes: 142 additions & 0 deletions
142
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,142 @@ | ||
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); | ||
const groupNameInput = screen.getByTestId('group-name'); | ||
userEvent.type(groupNameInput, 'test group name'); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText('emails.csv')).toBeInTheDocument(); | ||
expect(screen.getByText('Summary (1)')).toBeInTheDocument(); | ||
expect(screen.getByText('Total members to add')).toBeInTheDocument(); | ||
expect(screen.getByText('[email protected]')).toBeInTheDocument(); | ||
const formFeedbackText = 'Maximum members 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'); | ||
const error = new Error('An error occurred'); | ||
mockCreateGroup.mockRejectedValueOnce(error); | ||
mockInvite.mockRejectedValueOnce(error); | ||
|
||
render(<CreateGroupModalWrapper />); | ||
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 add')).toBeInTheDocument(); | ||
expect(screen.getByText('[email protected]')).toBeInTheDocument(); | ||
const formFeedbackText = 'Maximum members at a time: 1000'; | ||
expect(screen.queryByText(formFeedbackText)).not.toBeInTheDocument(); | ||
}, { timeout: EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY + 1000 }); | ||
|
||
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.