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: adds flex groups dropdown menu in assignment modal #1335

Merged
merged 8 commits into from
Oct 30, 2024
Merged
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