-
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: creates a modal to add members (#1363)
* feat: creates a modal to add members
- Loading branch information
1 parent
0847890
commit 1fa9064
Showing
27 changed files
with
1,171 additions
and
242 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,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; |
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,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; |
15 changes: 15 additions & 0 deletions
15
src/components/PeopleManagement/AddMembersModal/AddMemberModalSummaryDuplicate.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,15 @@ | ||
import React from 'react'; | ||
import { Stack, Icon } from '@openedx/paragon'; | ||
import { Error } from '@openedx/paragon/icons'; | ||
|
||
const AddMemberModalSummaryDuplicate = () => ( | ||
<Stack className="duplicate-warning" direction="horizontal" gap={3}> | ||
<Icon className="text-info-500" src={Error} /> | ||
<div> | ||
<div className="h4 mb-1">Only 1 invite per email address will be sent.</div> | ||
<span className="small">One or more duplicate emails were detected. Ensure that your entry is correct before proceeding.</span> | ||
</div> | ||
</Stack> | ||
); | ||
|
||
export default AddMemberModalSummaryDuplicate; |
10 changes: 10 additions & 0 deletions
10
src/components/PeopleManagement/AddMembersModal/AddMemberModalSummaryEmptyState.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,10 @@ | ||
import React from 'react'; | ||
|
||
const AddMemberModalSummaryEmptyState = () => ( | ||
<> | ||
<div className="h4 mb-0">You haven't uploaded any members yet.</div> | ||
<span className="small">Upload a CSV file or select members to get started.</span> | ||
</> | ||
); | ||
|
||
export default AddMemberModalSummaryEmptyState; |
15 changes: 15 additions & 0 deletions
15
src/components/PeopleManagement/AddMembersModal/AddMemberModalSummaryErrorState.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,15 @@ | ||
import React from 'react'; | ||
import { Stack, Icon } from '@openedx/paragon'; | ||
import { Error } from '@openedx/paragon/icons'; | ||
|
||
const AddMemberModalSummaryErrorState = () => ( | ||
<Stack direction="horizontal" gap={3}> | ||
<Icon className="text-danger" src={Error} /> | ||
<div> | ||
<div className="h4 mb-0">Members can't be added as entered.</div> | ||
<span className="small">Please check your file and try again.</span> | ||
</div> | ||
</Stack> | ||
); | ||
|
||
export default AddMemberModalSummaryErrorState; |
68 changes: 68 additions & 0 deletions
68
src/components/PeopleManagement/AddMembersModal/AddMemberModalSummaryLearnerList.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,68 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
import { | ||
Button, Stack, Icon, | ||
} from '@openedx/paragon'; | ||
import { Person } from '@openedx/paragon/icons'; | ||
|
||
import { MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT } from '../constants'; | ||
import { hasLearnerEmailsSummaryListTruncation } from '../utils'; | ||
|
||
const AddMemberModalSummaryLearnerList = ({ | ||
learnerEmails, | ||
}) => { | ||
const [isTruncated, setIsTruncated] = useState(hasLearnerEmailsSummaryListTruncation(learnerEmails)); | ||
const truncatedLearnerEmails = learnerEmails.slice(0, MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT); | ||
const displayedLearnerEmails = isTruncated ? truncatedLearnerEmails : learnerEmails; | ||
|
||
useEffect(() => { | ||
setIsTruncated(hasLearnerEmailsSummaryListTruncation(learnerEmails)); | ||
}, [learnerEmails]); | ||
|
||
const expandCollapseMessage = isTruncated | ||
? `Show ${learnerEmails.length - MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT} more` | ||
: 'Show less'; | ||
|
||
return ( | ||
<ul className="list-unstyled mb-0"> | ||
<Stack gap={2.5}> | ||
{displayedLearnerEmails.map((emailAddress) => ( | ||
<li key={uuidv4()} className="small"> | ||
<div className="d-flex justify-content-between"> | ||
<div style={{ maxWidth: '85%' }}> | ||
<Stack direction="horizontal" gap={2} className="align-items-center"> | ||
<Icon size="sm" src={Person} /> | ||
<div | ||
className="text-nowrap overflow-hidden font-weight-bold" | ||
style={{ textOverflow: 'ellipsis' }} | ||
title={emailAddress} | ||
data-hj-suppress | ||
> | ||
{emailAddress} | ||
</div> | ||
</Stack> | ||
</div> | ||
</div> | ||
</li> | ||
))} | ||
</Stack> | ||
{hasLearnerEmailsSummaryListTruncation(learnerEmails) && ( | ||
<Button | ||
variant="link" | ||
size="sm" | ||
className="mt-2.5" | ||
onClick={() => setIsTruncated(prevState => !prevState)} | ||
> | ||
{expandCollapseMessage} | ||
</Button> | ||
)} | ||
</ul> | ||
); | ||
}; | ||
|
||
AddMemberModalSummaryLearnerList.propTypes = { | ||
learnerEmails: PropTypes.arrayOf(PropTypes.string).isRequired, | ||
}; | ||
|
||
export default AddMemberModalSummaryLearnerList; |
136 changes: 136 additions & 0 deletions
136
src/components/PeopleManagement/AddMembersModal/AddMembersModal.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,136 @@ | ||
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: 'Add members', | ||
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); |
Oops, something went wrong.