Skip to content
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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Copy link
Contributor

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
Screenshot 2024-12-12 at 2 51 56 PM

);

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;

Check warning on line 24 in src/components/PeopleManagement/AddMembersBulkAction.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersBulkAction.jsx#L23-L24

Added lines #L23 - L24 were not covered by tests
}
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;
137 changes: 137 additions & 0 deletions src/components/PeopleManagement/AddMembersModal.jsx
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');

Check warning on line 31 in src/components/PeopleManagement/AddMembersModal.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModal.jsx#L30-L31

Added lines #L30 - L31 were not covered by tests
};
const queryClient = useQueryClient();
const {
isLoading,
enterpriseGroupLearners,
} = useAllEnterpriseGroupLearners(groupUuid);

const handleAddMembers = async () => {
setAddButtonState('pending');
try {
const requestBody = snakeCaseObject({

Check warning on line 42 in src/components/PeopleManagement/AddMembersModal.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModal.jsx#L40-L42

Added lines #L40 - L42 were not covered by tests
learnerEmails,
});
await LmsApiService.inviteEnterpriseLearnersToGroup(groupUuid, requestBody);
queryClient.invalidateQueries({

Check warning on line 46 in src/components/PeopleManagement/AddMembersModal.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModal.jsx#L45-L46

Added lines #L45 - L46 were not covered by tests
queryKey: learnerCreditManagementQueryKeys.group(groupUuid),
});
setAddButtonState('complete');
handleCloseAddMembersModal();

Check warning on line 50 in src/components/PeopleManagement/AddMembersModal.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModal.jsx#L49-L50

Added lines #L49 - L50 were not covered by tests
} catch (err) {
logError(err);
setAddButtonState('error');
openSystemErrorModal();

Check warning on line 54 in src/components/PeopleManagement/AddMembersModal.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModal.jsx#L52-L54

Added lines #L52 - L54 were not covered by tests
}
};

const handleEmailAddressesChange = useCallback((
value,
{ canInvite = false } = {},
) => {
setLearnerEmails(value);
setCanAddMembersGroup(canInvite);

Check warning on line 63 in src/components/PeopleManagement/AddMembersModal.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModal.jsx#L61-L63

Added lines #L61 - L63 were not covered by tests
}, []);

useEffect(() => {
setCanAddMembersGroup(false);
if (canAddMembers) {
setCanAddMembersGroup(true);

Check warning on line 69 in src/components/PeopleManagement/AddMembersModal.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModal.jsx#L69

Added line #L69 was not covered by tests
}
}, [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);
145 changes: 145 additions & 0 deletions src/components/PeopleManagement/AddMembersModalContent.jsx
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({

Check warning on line 27 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L24-L27

Added lines #L24 - L27 were not covered by tests
isValidInput: null,
lowerCasedEmails: [],
duplicateEmails: [],
emailsNotInOrg: [],
});
const { allEnterpriseLearners } = useEnterpriseLearners({ enterpriseUUID });

Check warning on line 33 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L33

Added line #L33 was not covered by tests

const handleAddMembersBulkAction = useCallback((value) => {

Check warning on line 35 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L35

Added line #L35 was not covered by tests
if (!value) {
setLearnerEmails([]);
onEmailAddressesChange([]);
return;

Check warning on line 39 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L37-L39

Added lines #L37 - L39 were not covered by tests
}
setLearnerEmails(prev => [...prev, ...value]);

Check warning on line 41 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L41

Added line #L41 was not covered by tests
}, [onEmailAddressesChange]);

const handleRemoveMembersBulkAction = useCallback((value) => {

Check warning on line 44 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L44

Added line #L44 was not covered by tests
if (!value) {
setLearnerEmails([]);
onEmailAddressesChange([]);
return;

Check warning on line 48 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L46-L48

Added lines #L46 - L48 were not covered by tests
}
setLearnerEmails(prev => prev.filter((el) => !value.includes(el)));

Check warning on line 50 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L50

Added line #L50 was not covered by tests
}, [onEmailAddressesChange]);

const handleEmailAddressesChanged = useCallback((value) => {

Check warning on line 53 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L53

Added line #L53 was not covered by tests
if (!value) {
setLearnerEmails([]);
onEmailAddressesChange([]);
return;

Check warning on line 57 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L55-L57

Added lines #L55 - L57 were not covered by tests
}
// 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);

Check warning on line 61 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L60-L61

Added lines #L60 - L61 were not covered by tests
}, [onEmailAddressesChange]);

const debouncedHandleEmailAddressesChanged = useMemo(
() => debounce(handleEmailAddressesChanged, EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY),

Check warning on line 65 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L64-L65

Added lines #L64 - L65 were not covered by tests
[handleEmailAddressesChanged],
);

useEffect(() => {
debouncedHandleEmailAddressesChanged(emailAddressesInputValue);

Check warning on line 70 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L69-L70

Added lines #L69 - L70 were not covered by tests
}, [emailAddressesInputValue, debouncedHandleEmailAddressesChanged]);

// Validate the learner emails emails from user input whenever it changes
useEffect(() => {
const inviteMetadata = isInviteEmailAddressesInputValueValid({

Check warning on line 75 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L74-L75

Added lines #L74 - L75 were not covered by tests
learnerEmails,
allEnterpriseLearners,
});
setMemberInviteMetadata(inviteMetadata);

Check warning on line 79 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L79

Added line #L79 was not covered by tests
if (inviteMetadata.canInvite) {
onEmailAddressesChange(learnerEmails, { canInvite: true });
} else {
onEmailAddressesChange([]);

Check warning on line 83 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L81-L83

Added lines #L81 - L83 were not covered by tests
}
}, [onEmailAddressesChange, learnerEmails, allEnterpriseLearners]);

return (

Check warning on line 87 in src/components/PeopleManagement/AddMembersModalContent.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/PeopleManagement/AddMembersModalContent.jsx#L87

Added line #L87 was not covered by tests
<Container size="lg" className="py-3">
<h3>
<FormattedMessage
id="people.management.page.create.group.section.header"
defaultMessage="Create a custom group of members"
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
Copy link
Contributor

Choose a reason for hiding this comment

The 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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I upload a large CSV, I'm getting two error states. I don't see any examples of this in Figma, and I think we might need to talk to Sophia about which one should be present
Screenshot 2024-12-12 at 2 48 32 PM

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;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import InviteSummaryCount from '../learner-credit-management/invite-modal/Invite
import FileUpload from '../learner-credit-management/invite-modal/FileUpload';
import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isInviteEmailAddressesInputValueValid } from '../learner-credit-management/cards/data';
import { MAX_LENGTH_GROUP_NAME } from './constants';
import EnterpriseCustomerUserDatatable from '../learner-credit-management/invite-modal/EnterpriseCustomerUserDatatable';
import EnterpriseCustomerUserDatatable from './EnterpriseCustomerUserDatatable';
import { useEnterpriseLearners } from '../learner-credit-management/data';

const CreateGroupModalContent = ({
Expand Down
Loading
Loading