-
Notifications
You must be signed in to change notification settings - Fork 33
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: creates a modal to add members #1363
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Button } from '@openedx/paragon'; | ||
import { Add } from '@openedx/paragon/icons'; | ||
import PropTypes from 'prop-types'; | ||
|
||
const AddMemberTableAction = ({ openModal }) => ( | ||
<Button iconBefore={Add} onClick={openModal} variant="outline-primary">Add members</Button> | ||
); | ||
|
||
AddMemberTableAction.propTypes = { | ||
openModal: PropTypes.func.isRequired, | ||
}; | ||
|
||
export default AddMemberTableAction; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import PropTypes from 'prop-types'; | ||
import { StatefulButton } from '@openedx/paragon'; | ||
import { useIntl } from '@edx/frontend-platform/i18n'; | ||
import { useGetAllEnterpriseLearnerEmails } from './data/hooks/useEnterpriseLearnersTableData'; | ||
import { getSelectedEmailsByRow } from './utils'; | ||
|
||
const AddMembersBulkAction = ({ | ||
isEntireTableSelected, | ||
selectedFlatRows, | ||
onHandleAddMembersBulkAction, | ||
enterpriseId, | ||
enterpriseGroupLearners, | ||
}) => { | ||
const intl = useIntl(); | ||
const { fetchLearnerEmails, addButtonState } = useGetAllEnterpriseLearnerEmails({ | ||
enterpriseId, | ||
isEntireTableSelected, | ||
onHandleAddMembersBulkAction, | ||
enterpriseGroupLearners, | ||
}); | ||
const handleOnClick = () => { | ||
if (isEntireTableSelected) { | ||
fetchLearnerEmails(); | ||
return; | ||
} | ||
const addedMemberEmails = enterpriseGroupLearners.map(learner => learner.memberDetails.userEmail); | ||
const emails = getSelectedEmailsByRow(selectedFlatRows).filter(email => !addedMemberEmails.includes(email)); | ||
onHandleAddMembersBulkAction(emails); | ||
}; | ||
|
||
return ( | ||
<StatefulButton | ||
labels={{ | ||
default: intl.formatMessage({ | ||
id: 'people.management.add.new.group.modal.button', | ||
defaultMessage: 'Add', | ||
description: 'Button state text for adding members from datatable', | ||
}), | ||
pending: intl.formatMessage({ | ||
id: 'people.management.add.new.group.modal.pending', | ||
defaultMessage: 'Adding...', | ||
description: 'Button state text for adding members from datatable', | ||
}), | ||
complete: intl.formatMessage({ | ||
id: 'people.management.add.new.group.modal.complete', | ||
defaultMessage: 'Add', | ||
description: 'Button state text for adding members from datatable', | ||
}), | ||
error: intl.formatMessage({ | ||
id: 'people.management.add.new.group.modal.try.again', | ||
defaultMessage: 'Try again', | ||
description: 'Button state text for trying to add members again', | ||
}), | ||
}} | ||
state={addButtonState} | ||
onClick={handleOnClick} | ||
disabledStates={['pending']} | ||
/> | ||
); | ||
}; | ||
|
||
AddMembersBulkAction.propTypes = { | ||
selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()).isRequired, | ||
enterpriseId: PropTypes.string.isRequired, | ||
onHandleAddMembersBulkAction: PropTypes.func.isRequired, | ||
isEntireTableSelected: PropTypes.bool, | ||
enterpriseGroupLearners: PropTypes.arrayOf(PropTypes.string), | ||
}; | ||
|
||
export default AddMembersBulkAction; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
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 { useQueryClient } from '@tanstack/react-query'; | ||
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 SystemErrorAlertModal from '../learner-credit-management/cards/assignment-allocation-status-modals/SystemErrorAlertModal'; | ||
import AddMembersModalContent from './AddMembersModalContent'; | ||
import { learnerCreditManagementQueryKeys } from '../learner-credit-management/data'; | ||
import { useAllEnterpriseGroupLearners } from './data/hooks'; | ||
|
||
const AddMembersModal = ({ | ||
isModalOpen, | ||
closeModal, | ||
enterpriseUUID, | ||
groupName, | ||
groupUuid, | ||
}) => { | ||
const intl = useIntl(); | ||
const [learnerEmails, setLearnerEmails] = useState([]); | ||
const [addButtonState, setAddButtonState] = useState('default'); | ||
const [canAddMembers, setCanAddMembersGroup] = useState(false); | ||
const [isSystemErrorModalOpen, openSystemErrorModal, closeSystemErrorModal] = useToggle(false); | ||
const handleCloseAddMembersModal = () => { | ||
closeModal(); | ||
setAddButtonState('default'); | ||
}; | ||
const queryClient = useQueryClient(); | ||
const { | ||
isLoading, | ||
enterpriseGroupLearners, | ||
} = useAllEnterpriseGroupLearners(groupUuid); | ||
|
||
const handleAddMembers = async () => { | ||
setAddButtonState('pending'); | ||
try { | ||
const requestBody = snakeCaseObject({ | ||
learnerEmails, | ||
}); | ||
await LmsApiService.inviteEnterpriseLearnersToGroup(groupUuid, requestBody); | ||
queryClient.invalidateQueries({ | ||
queryKey: learnerCreditManagementQueryKeys.group(groupUuid), | ||
}); | ||
setAddButtonState('complete'); | ||
handleCloseAddMembersModal(); | ||
} catch (err) { | ||
logError(err); | ||
setAddButtonState('error'); | ||
openSystemErrorModal(); | ||
} | ||
}; | ||
|
||
const handleEmailAddressesChange = useCallback(( | ||
value, | ||
{ canInvite = false } = {}, | ||
) => { | ||
setLearnerEmails(value); | ||
setCanAddMembersGroup(canInvite); | ||
}, []); | ||
|
||
useEffect(() => { | ||
setCanAddMembersGroup(false); | ||
if (canAddMembers) { | ||
setCanAddMembersGroup(true); | ||
} | ||
}, [canAddMembers]); | ||
|
||
return ( | ||
<div> | ||
{!isLoading ? ( | ||
<div> | ||
<FullscreenModal | ||
className="stepper-modal bg-light-200" | ||
isOpen={isModalOpen} | ||
onClose={handleCloseAddMembersModal} | ||
title={intl.formatMessage({ | ||
id: 'peopleManagement.tab.add.members.modal.title', | ||
defaultMessage: 'New group', | ||
description: 'Title for adding members modal', | ||
})} | ||
footerNode={( | ||
<ActionRow> | ||
<ActionRow.Spacer /> | ||
<Button variant="tertiary" onClick={handleCloseAddMembersModal}>Cancel</Button> | ||
<StatefulButton | ||
labels={{ | ||
default: 'Add', | ||
pending: 'Adding...', | ||
complete: 'Added', | ||
error: 'Try again', | ||
}} | ||
variant="primary" | ||
state={addButtonState} | ||
disabled={!canAddMembers} | ||
onClick={handleAddMembers} | ||
/> | ||
</ActionRow> | ||
)} | ||
> | ||
<AddMembersModalContent | ||
groupName={groupName} | ||
onEmailAddressesChange={handleEmailAddressesChange} | ||
isGroupInvite | ||
enterpriseUUID={enterpriseUUID} | ||
enterpriseGroupLearners={enterpriseGroupLearners} | ||
/> | ||
</FullscreenModal> | ||
<SystemErrorAlertModal | ||
isErrorModalOpen={isSystemErrorModalOpen} | ||
closeErrorModal={closeSystemErrorModal} | ||
closeAssignmentModal={handleCloseAddMembersModal} | ||
retry={handleAddMembers} | ||
/> | ||
</div> | ||
) : null} | ||
</div> | ||
); | ||
}; | ||
|
||
AddMembersModal.propTypes = { | ||
enterpriseUUID: PropTypes.string.isRequired, | ||
isModalOpen: PropTypes.bool.isRequired, | ||
closeModal: PropTypes.func.isRequired, | ||
groupUuid: PropTypes.string.isRequired, | ||
groupName: PropTypes.string, | ||
}; | ||
|
||
const mapStateToProps = state => ({ | ||
enterpriseUUID: state.portalConfiguration.enterpriseId, | ||
}); | ||
|
||
export default connect(mapStateToProps)(AddMembersModal); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import React, { | ||
useCallback, useEffect, useMemo, useState, | ||
} from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import debounce from 'lodash.debounce'; | ||
import { | ||
Col, Container, Row, | ||
} from '@openedx/paragon'; | ||
import { FormattedMessage } from '@edx/frontend-platform/i18n'; | ||
|
||
import InviteModalSummary from '../learner-credit-management/invite-modal/InviteModalSummary'; | ||
import InviteSummaryCount from '../learner-credit-management/invite-modal/InviteSummaryCount'; | ||
import FileUpload from '../learner-credit-management/invite-modal/FileUpload'; | ||
import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isInviteEmailAddressesInputValueValid } from '../learner-credit-management/cards/data'; | ||
import EnterpriseCustomerUserDatatable from './EnterpriseCustomerUserDatatable'; | ||
import { useEnterpriseLearners } from '../learner-credit-management/data'; | ||
|
||
const AddMembersModalContent = ({ | ||
onEmailAddressesChange, | ||
isGroupInvite, | ||
enterpriseUUID, | ||
groupName, | ||
enterpriseGroupLearners, | ||
}) => { | ||
const [learnerEmails, setLearnerEmails] = useState([]); | ||
const [emailAddressesInputValue, setEmailAddressesInputValue] = useState(''); | ||
const [memberInviteMetadata, setMemberInviteMetadata] = useState({ | ||
isValidInput: null, | ||
lowerCasedEmails: [], | ||
duplicateEmails: [], | ||
emailsNotInOrg: [], | ||
}); | ||
const { allEnterpriseLearners } = useEnterpriseLearners({ enterpriseUUID }); | ||
|
||
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([]); | ||
onEmailAddressesChange([]); | ||
return; | ||
} | ||
// handles csv upload value and formats emails into an array of strings | ||
const emails = value.split('\n').map((email) => email.trim()).filter((email) => email.length > 0); | ||
setLearnerEmails(emails); | ||
}, [onEmailAddressesChange]); | ||
|
||
const debouncedHandleEmailAddressesChanged = useMemo( | ||
() => debounce(handleEmailAddressesChanged, EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY), | ||
[handleEmailAddressesChanged], | ||
); | ||
|
||
useEffect(() => { | ||
debouncedHandleEmailAddressesChanged(emailAddressesInputValue); | ||
}, [emailAddressesInputValue, debouncedHandleEmailAddressesChanged]); | ||
|
||
// Validate the learner emails emails from user input whenever it changes | ||
useEffect(() => { | ||
const inviteMetadata = isInviteEmailAddressesInputValueValid({ | ||
learnerEmails, | ||
allEnterpriseLearners, | ||
}); | ||
setMemberInviteMetadata(inviteMetadata); | ||
if (inviteMetadata.canInvite) { | ||
onEmailAddressesChange(learnerEmails, { canInvite: true }); | ||
} else { | ||
onEmailAddressesChange([]); | ||
} | ||
}, [onEmailAddressesChange, learnerEmails, allEnterpriseLearners]); | ||
|
||
return ( | ||
<Container size="lg" className="py-3"> | ||
<h3> | ||
<FormattedMessage | ||
id="people.management.page.create.group.section.header" | ||
defaultMessage="Create a custom group of members" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This title isn't needed for the Add members modal |
||
description="Header for the section to create a new group." | ||
/> | ||
</h3> | ||
<Row> | ||
<Col> | ||
<h4 className="mt-4">Add new members to your group</h4> | ||
<p>Only members registered with your organization can be added to your group. Learn more</p> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even though we might not have the help center doc yet, I think we could make this a hyperlink that just points to edx.org or something |
||
<h4 className="mt-4">Group Name</h4> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Group name" should be all caps, should add className="text-uppercase" |
||
<p className="font-weight-bold">{groupName}</p> | ||
</Col> | ||
<Col /> | ||
</Row> | ||
<Row> | ||
<Col> | ||
<h4 className="mt-2">Select group members</h4> | ||
<p> | ||
<FormattedMessage | ||
id="people.management.page.create.group.csv.upload" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
defaultMessage="Upload a CSV or select members from the table below." | ||
description="Upload csv section and datatable with members to add to the new group." | ||
/> | ||
</p> | ||
<FileUpload | ||
memberInviteMetadata={memberInviteMetadata} | ||
setEmailAddressesInputValue={setEmailAddressesInputValue} | ||
/> | ||
</Col> | ||
<Col> | ||
<h4>Details</h4> | ||
<InviteModalSummary isGroupInvite={isGroupInvite} memberInviteMetadata={memberInviteMetadata} /> | ||
<InviteSummaryCount memberInviteMetadata={memberInviteMetadata} /> | ||
<hr className="my-4" /> | ||
</Col> | ||
</Row> | ||
<EnterpriseCustomerUserDatatable | ||
onHandleAddMembersBulkAction={handleAddMembersBulkAction} | ||
onHandleRemoveMembersBulkAction={handleRemoveMembersBulkAction} | ||
learnerEmails={learnerEmails} | ||
enterpriseGroupLearners={enterpriseGroupLearners} | ||
/> | ||
</Container> | ||
); | ||
}; | ||
|
||
AddMembersModalContent.propTypes = { | ||
onEmailAddressesChange: PropTypes.func.isRequired, | ||
isGroupInvite: PropTypes.bool, | ||
enterpriseUUID: PropTypes.string.isRequired, | ||
groupName: PropTypes.string, | ||
enterpriseGroupLearners: PropTypes.arrayOf(PropTypes.shape({})), | ||
}; | ||
|
||
export default AddMembersModalContent; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not actually seeing this button as a table action? The only way I'm able to open up the add members modal is a weird stray edit icon on the Group Detail card after the "View group progress" button