Skip to content

Commit

Permalink
feat: creates a modal to add members (#1363)
Browse files Browse the repository at this point in the history
* feat: creates a modal to add members
  • Loading branch information
katrinan029 authored Jan 7, 2025
1 parent 0847890 commit 1fa9064
Show file tree
Hide file tree
Showing 27 changed files with 1,171 additions and 242 deletions.
13 changes: 13 additions & 0 deletions src/components/PeopleManagement/AddMemberTableAction.jsx
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;
70 changes: 70 additions & 0 deletions src/components/PeopleManagement/AddMembersBulkAction.jsx
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,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;
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&apos;t uploaded any members yet.</div>
<span className="small">Upload a CSV file or select members to get started.</span>
</>
);

export default AddMemberModalSummaryEmptyState;
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&apos;t be added as entered.</div>
<span className="small">Please check your file and try again.</span>
</div>
</Stack>
);

export default AddMemberModalSummaryErrorState;
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 src/components/PeopleManagement/AddMembersModal/AddMembersModal.jsx
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);
Loading

0 comments on commit 1fa9064

Please sign in to comment.