Skip to content

Commit

Permalink
feat: adds flex groups dropdown menu in assignment modal (#1335)
Browse files Browse the repository at this point in the history
* feat: adds group selection to assignment modal
  • Loading branch information
katrinan029 authored Oct 30, 2024
1 parent 4f9d197 commit 4e72019
Show file tree
Hide file tree
Showing 16 changed files with 620 additions and 10 deletions.
2 changes: 1 addition & 1 deletion src/components/PeopleManagement/EditGroupNameModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ActionRow, Form, ModalDialog, Spinner, StatefulButton, useToggle,
} from '@openedx/paragon';

import MAX_LENGTH_GROUP_NAME from './constants';
import { MAX_LENGTH_GROUP_NAME } from './constants';
import LmsApiService from '../../data/services/LmsApiService';
import GeneralErrorModal from './GeneralErrorModal';

Expand Down
1 change: 1 addition & 0 deletions src/components/PeopleManagement/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export const MAX_LENGTH_GROUP_NAME = 60;

export const GROUP_TYPE_BUDGET = 'budget';
export const GROUP_TYPE_FLEX = 'flex';
export const GROUP_DROPDOWN_TEXT = 'Select group';
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@ import AssignmentModalSummary from './AssignmentModalSummary';
import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isAssignEmailAddressesInputValueValid } from '../cards/data';
import AssignmentAllocationHelpCollapsibles from './AssignmentAllocationHelpCollapsibles';
import EVENT_NAMES from '../../../eventTracking';
import AssignmentModalFlexGroup from './AssignmentModalFlexGroup';
import useGroupDropdownToggle from '../data/hooks/useGroupDropdownToggle';
import { GROUP_DROPDOWN_TEXT } from '../../PeopleManagement/constants';

const AssignmentModalContent = ({
enterpriseId, course, courseRun, onEmailAddressesChange,
enterpriseId,
course,
courseRun,
onEmailAddressesChange,
enterpriseFlexGroups,
onGroupSelectionsChanged,
enterpriseFeatures,
}) => {
const shouldShowGroupsDropdown = enterpriseFeatures.enterpriseGroupsV2 && enterpriseFlexGroups?.length > 0;
const { subsidyAccessPolicyId } = useBudgetId();
const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(subsidyAccessPolicyId);
const spendAvailable = subsidyAccessPolicy.aggregates.spendAvailableUsd;
Expand All @@ -28,6 +38,22 @@ const AssignmentModalContent = ({
const [assignmentAllocationMetadata, setAssignmentAllocationMetadata] = useState({});
const intl = useIntl();
const { contentPrice } = courseRun;
const [groupMemberEmails, setGroupMemberEmails] = useState([]);
const [checkedGroups, setCheckedGroups] = useState({});
const [dropdownToggleLabel, setDropdownToggleLabel] = useState(GROUP_DROPDOWN_TEXT);
const {
dropdownRef,
handleCheckedGroupsChanged,
handleGroupsChanged,
handleSubmitGroup,
} = useGroupDropdownToggle({
checkedGroups,
dropdownToggleLabel,
onGroupSelectionsChanged,
setCheckedGroups,
setDropdownToggleLabel,
setGroupMemberEmails,
});
const handleEmailAddressInputChange = (e) => {
const inputValue = e.target.value;
setEmailAddressesInputValue(inputValue);
Expand All @@ -51,10 +77,22 @@ const AssignmentModalContent = ({
debouncedHandleEmailAddressesChanged(emailAddressesInputValue);
}, [emailAddressesInputValue, debouncedHandleEmailAddressesChanged]);

useEffect(() => {
handleGroupsChanged(checkedGroups);
const selectedGroups = Object.keys(checkedGroups).filter(group => checkedGroups[group].checked === true);
if (selectedGroups.length === 1) {
setDropdownToggleLabel(`${checkedGroups[selectedGroups[0]]?.name} (${checkedGroups[selectedGroups[0]]?.memberEmails.length})`);
} else if (selectedGroups.length > 1) {
setDropdownToggleLabel(`${selectedGroups.length} groups selected`);
} else {
setDropdownToggleLabel(GROUP_DROPDOWN_TEXT);
}
}, [checkedGroups, handleGroupsChanged]);

// Validate the learner emails from user input whenever it changes
useEffect(() => {
const allocationMetadata = isAssignEmailAddressesInputValueValid({
learnerEmails,
learnerEmails: [...learnerEmails, ...groupMemberEmails],
remainingBalance: spendAvailable,
contentPrice,
});
Expand All @@ -68,10 +106,20 @@ const AssignmentModalContent = ({
}
if (allocationMetadata.canAllocate) {
onEmailAddressesChange(learnerEmails, { canAllocate: true });
onGroupSelectionsChanged(groupMemberEmails, { canAllocate: true });
} else {
onEmailAddressesChange([]);
onGroupSelectionsChanged([]);
}
}, [onEmailAddressesChange, learnerEmails, contentPrice, spendAvailable, enterpriseId]);
}, [
onEmailAddressesChange,
learnerEmails,
contentPrice,
spendAvailable,
enterpriseId,
groupMemberEmails,
onGroupSelectionsChanged,
]);

return (
<Container size="lg" className="py-3">
Expand All @@ -97,6 +145,16 @@ const AssignmentModalContent = ({
description="Header for the section where we assign a course to learners"
/>
</h4>
{shouldShowGroupsDropdown && (
<AssignmentModalFlexGroup
checkedGroups={checkedGroups}
dropdownRef={dropdownRef}
dropdownToggleLabel={dropdownToggleLabel}
enterpriseFlexGroups={enterpriseFlexGroups}
onCheckedGroupsChanged={handleCheckedGroupsChanged}
onHandleSubmitGroup={handleSubmitGroup}
/>
)}
<Form.Group className="mb-5">
<Form.Control
as="textarea"
Expand Down Expand Up @@ -143,7 +201,7 @@ const AssignmentModalContent = ({
</h4>
<AssignmentModalSummary
courseRun={courseRun}
learnerEmails={learnerEmails}
learnerEmails={[...learnerEmails, ...groupMemberEmails]}
assignmentAllocationMetadata={assignmentAllocationMetadata}
/>
<hr className="my-4" />
Expand Down Expand Up @@ -209,10 +267,20 @@ AssignmentModalContent.propTypes = {
contentPrice: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}).isRequired,
onEmailAddressesChange: PropTypes.func.isRequired,
onGroupSelectionsChanged: PropTypes.func.isRequired,
enterpriseFlexGroups: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
uuid: PropTypes.string,
acceptedMembersCount: PropTypes.number,
})),
enterpriseFeatures: PropTypes.shape({
enterpriseGroupsV2: PropTypes.bool.isRequired,
}),
};

const mapStateToProps = state => ({
enterpriseId: state.portalConfiguration.enterpriseId,
enterpriseFeatures: state.portalConfiguration.enterpriseFeatures,
});

export default connect(mapStateToProps)(AssignmentModalContent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import {
Form, MenuItem, Dropdown, Button,
} from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';

const AssignmentModalFlexGroup = ({
enterpriseFlexGroups,
onCheckedGroupsChanged,
checkedGroups,
onHandleSubmitGroup,
dropdownToggleLabel,
dropdownRef,
}) => {
const renderFlexGroupSelection = enterpriseFlexGroups.map(flexGroup => (
<MenuItem
as={Form.Checkbox}
className="group-dropdown mt-2 mb-2"
key={flexGroup.uuid}
onChange={onCheckedGroupsChanged}
checked={checkedGroups[flexGroup.uuid]?.checked}
value={flexGroup.name}
id={flexGroup.uuid}
>
{flexGroup.name} ({flexGroup.acceptedMembersCount})
</MenuItem>
));

return (
<Form.Group className="group-dropdown mb-4.5 pr-1.5">
<Dropdown ref={dropdownRef} autoClose="outside" className="group-dropdown">
Groups
<Dropdown.Toggle variant="outline-primary" id="group-select-toggle" className="group-dropdown mt-2">
{dropdownToggleLabel}
</Dropdown.Toggle>
<Dropdown.Menu className="pl-3 pr-3 group-dropdown">
{renderFlexGroupSelection}
<Button className="mt-3 justify-content-center" block onClick={onHandleSubmitGroup}>Apply selections</Button>
</Dropdown.Menu>
</Dropdown>
<Form.Control.Feedback>
<FormattedMessage
id="lcm.budget.detail.page.catalog.tab.assign.course.section.assign.to.flex.group.help.text"
defaultMessage="Select one or more group to add its members to the assignment."
description="Help text for the flex group drop down menu to add learners from selected group."
/>
</Form.Control.Feedback>
</Form.Group>
);
};

AssignmentModalFlexGroup.propTypes = {
checkedGroups: PropTypes.shape({
id: PropTypes.string,
memberEmails: PropTypes.arrayOf(PropTypes.string),
name: PropTypes.string,
checked: PropTypes.bool,
}).isRequired,
onHandleSubmitGroup: PropTypes.func.isRequired,
onCheckedGroupsChanged: PropTypes.func.isRequired,
enterpriseFlexGroups: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
uuid: PropTypes.string,
acceptedMembersCount: PropTypes.number,
})),
dropdownToggleLabel: PropTypes.string.isRequired,
dropdownRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
};

export default AssignmentModalFlexGroup;
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const AssignmentModalSummaryErrorState = () => (
description="Error message when course assignment fails due to invalid learner emails."
/>
</span>
</div>.
</div>
</Stack>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import EVENT_NAMES from '../../../eventTracking';
import { BudgetDetailPageContext } from '../BudgetDetailPageWrapper';
import {
getAssignableCourseRuns, learnerCreditManagementQueryKeys, LEARNER_CREDIT_ROUTE, useBudgetId,
useSubsidyAccessPolicy,
useSubsidyAccessPolicy, useEnterpriseFlexGroups,
} from '../data';
import AssignmentModalContent from './AssignmentModalContent';
import CreateAllocationErrorAlertModals from './CreateAllocationErrorAlertModals';
Expand All @@ -41,10 +41,12 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => {
const { subsidyAccessPolicyId } = useBudgetId();
const [isOpen, open, close] = useToggle(false);
const [learnerEmails, setLearnerEmails] = useState([]);
const [groupLearnerEmails, setGroupLearnerEmails] = useState([]);
const [canAllocateAssignments, setCanAllocateAssignments] = useState(false);
const [assignButtonState, setAssignButtonState] = useState('default');
const [createAssignmentsErrorReason, setCreateAssignmentsErrorReason] = useState();
const [assignmentRun, setAssignmentRun] = useState();
const { data: enterpriseFlexGroups } = useEnterpriseFlexGroups(enterpriseId);
const {
successfulAssignmentToast: { displayToastForAssignmentAllocation },
} = useContext(BudgetDetailPageContext);
Expand Down Expand Up @@ -119,6 +121,14 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => {
setCanAllocateAssignments(canAllocate);
}, []);

const handleGroupSelectionsChanged = useCallback((
value,
{ canAllocate = false } = {},
) => {
setGroupLearnerEmails(value);
setCanAllocateAssignments(canAllocate);
}, []);

const onSuccessEnterpriseTrackEvents = ({
totalLearnersAllocated,
totalLearnersAlreadyAllocated,
Expand All @@ -142,7 +152,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => {
const payload = snakeCaseObject({
contentPriceCents: assignmentRun.contentPrice * 100, // Convert to USD cents
contentKey: assignmentRun.key,
learnerEmails,
learnerEmails: [...learnerEmails, ...groupLearnerEmails],
});
const mutationArgs = {
subsidyAccessPolicyId,
Expand Down Expand Up @@ -198,7 +208,7 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => {
...sharedEnterpriseTrackEventMetadata,
contentKey: assignmentRun.key,
parentContentKey: course.key,
totalAllocatedLearners: learnerEmails.length,
totalAllocatedLearners: learnerEmails.length + groupLearnerEmails.length,
errorStatus: httpErrorStatus,
errorReason,
response: err,
Expand Down Expand Up @@ -316,6 +326,8 @@ const NewAssignmentModalButton = ({ enterpriseId, course, children }) => {
course={course}
courseRun={assignmentRun}
onEmailAddressesChange={handleEmailAddressesChanged}
enterpriseFlexGroups={enterpriseFlexGroups}
onGroupSelectionsChanged={handleGroupSelectionsChanged}
/>
</FullscreenModal>
<CreateAllocationErrorAlertModals
Expand Down
Loading

0 comments on commit 4e72019

Please sign in to comment.