Skip to content

Commit 1142844

Browse files
feat: add/unenroll learners
1 parent 6ea0c5b commit 1142844

File tree

7 files changed

+175
-13
lines changed

7 files changed

+175
-13
lines changed

src/enrollments/EnrollmentsPage.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import messages from './messages';
66
import EnrollmentsList from './components/EnrollmentsList';
77
import EnrollmentStatusModal from './components/EnrollmentStatusModal';
88
import UnenrollModal from './components/UnenrollModal';
9+
import EnrollLearnersModal from './components/EnrollLearnersModal';
910
import { Learner } from './types';
1011

1112
const EnrollmentsPage = () => {
1213
const intl = useIntl();
1314
const [isEnrollmentStatusModalOpen, setIsEnrollmentStatusModalOpen] = useState(false);
15+
const [isEnrollLearnersModalOpen, setIsEnrollLearnersModalOpen] = useState(false);
1416
const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false);
1517
const [selectedLearner, setSelectedLearner] = useState<Learner | null>(null);
1618

@@ -32,6 +34,10 @@ const EnrollmentsPage = () => {
3234
setIsEnrollmentStatusModalOpen(false);
3335
};
3436

37+
const handleEnrollLearners = () => {
38+
setIsEnrollLearnersModalOpen(true);
39+
};
40+
3541
return (
3642
<div className="my-4.5 mx-4">
3743
<div className="d-flex justify-content-between align-items-center">
@@ -44,12 +50,13 @@ const EnrollmentsPage = () => {
4450
onClick={handleMoreButton}
4551
/>
4652
<Button variant="outline-primary">+ {intl.formatMessage(messages.addBetaTesters)}</Button>
47-
<Button>+ {intl.formatMessage(messages.enrollLearners)}</Button>
53+
<Button onClick={handleEnrollLearners}>+ {intl.formatMessage(messages.enrollLearners)}</Button>
4854
</ActionRow>
4955
</div>
5056
<EnrollmentsList onUnenroll={handleUnenroll} />
5157
<EnrollmentStatusModal isOpen={isEnrollmentStatusModalOpen} onClose={handleCloseEnrollmentStatusModal} />
5258
<UnenrollModal isOpen={isUnenrollModalOpen} learner={selectedLearner} onClose={handleUnenrollModalClose} />
59+
<EnrollLearnersModal isOpen={isEnrollLearnersModalOpen} onClose={() => setIsEnrollLearnersModalOpen(false)} onEnrollSuccess={() => {}} />
5360
</div>
5461
);
5562
};
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { useState } from 'react';
2+
import { useParams } from 'react-router-dom';
3+
import { useIntl } from '@openedx/frontend-base';
4+
import { Button, FormControl, Alert, ModalDialog } from '@openedx/paragon';
5+
import messages from '../messages';
6+
import { useEnrollLearners } from '../data/apiHook';
7+
import { FormCheckbox, FormCheckboxSet } from '@openedx/paragon/dist/Form';
8+
9+
export interface EnrollLearnersModalProps {
10+
isOpen: boolean,
11+
onClose: () => void,
12+
onEnrollSuccess: () => void,
13+
}
14+
15+
const EnrollLearnersModal = ({ isOpen, onClose, onEnrollSuccess }: EnrollLearnersModalProps) => {
16+
const intl = useIntl();
17+
const { courseId = '' } = useParams<{ courseId: string }>();
18+
const { mutate: enrollLearners } = useEnrollLearners(courseId);
19+
const [isEnrolling, setIsEnrolling] = useState(false);
20+
const [emails, setEmails] = useState('');
21+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
22+
23+
const handleEnroll = () => {
24+
setIsEnrolling(true);
25+
setErrorMessage(null);
26+
const emailList = emails.split(',').map(email => email.trim()).filter(email => email);
27+
28+
enrollLearners(emailList, {
29+
onSuccess: () => {
30+
setIsEnrolling(false);
31+
onEnrollSuccess();
32+
onClose();
33+
},
34+
onError: (error) => {
35+
setIsEnrolling(false);
36+
// setErrorMessage(intl.formatMessage(messages.enrollLearnersError));
37+
console.error(error);
38+
}
39+
});
40+
};
41+
42+
return (
43+
<ModalDialog isOpen={isOpen} onClose={onClose} isOverflowVisible={false} title={intl.formatMessage(messages.enrollLearners)}>
44+
<ModalDialog.Header className="border-light-700 border-bottom">
45+
<h3 className="text-primary-500">{intl.formatMessage(messages.enrollLearners)}</h3>
46+
</ModalDialog.Header>
47+
<ModalDialog.Body className="py-4">
48+
<p className="text-gray-700 x-small mb-2">{intl.formatMessage(messages.enrollLearnerInstructions)}</p>
49+
<FormControl
50+
as="textarea"
51+
rows={4}
52+
placeholder={intl.formatMessage(messages.enrollLearnersPlaceholder)}
53+
onChange={(e) => setEmails(e.target.value)}
54+
/>
55+
<FormCheckboxSet isInline className="mt-3 text-primary-500">
56+
<FormCheckbox controlClassName="border-primary-500">Auto Enroll</FormCheckbox>
57+
<FormCheckbox controlClassName="border-primary-500" className="ml-4">Notify Users by Email</FormCheckbox>
58+
</FormCheckboxSet>
59+
{errorMessage && <Alert variant="danger" className="mt-3">{errorMessage}</Alert>}
60+
</ModalDialog.Body>
61+
<ModalDialog.Footer className="border-light-700 border-top">
62+
<Button variant="tertiary" onClick={onClose} disabled={isEnrolling}>
63+
{intl.formatMessage(messages.cancelButton)}
64+
</Button>
65+
<Button className="ml-2" variant="primary" onClick={handleEnroll} disabled={emails.trim().length === 0 || isEnrolling}>
66+
{intl.formatMessage(messages.saveButton)}
67+
</Button>
68+
</ModalDialog.Footer>
69+
</ModalDialog>
70+
);
71+
};
72+
73+
export default EnrollLearnersModal;

src/enrollments/components/EnrollmentStatusModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const EnrollmentStatusModal = ({ isOpen, onClose }: EnrollmentStatusModalProps)
2626
<ModalDialog.Body className="py-4">
2727
<p>{intl.formatMessage(messages.addLearnerInstructions)}</p>
2828
<FormControl
29-
placeholder={intl.formatMessage(messages.enrollLearnersPlaceholder)}
29+
placeholder={intl.formatMessage(messages.enrollmentStatusPlaceholder)}
3030
value={learnerIdentifier}
3131
onChange={(e) => setLearnerIdentifier(e.target.value)}
3232
/>
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
1+
import { useIntl } from '@openedx/frontend-base';
2+
import { Button, ModalDialog } from '@openedx/paragon';
3+
import messages from '../messages';
14
import { Learner } from '../types';
5+
26
interface UnenrollModalProps {
37
learner: Learner | null,
48
isOpen: boolean,
59
onClose: () => void,
610
}
711

812
const UnenrollModal = ({ learner, isOpen, onClose }: UnenrollModalProps) => {
9-
console.log(learner, isOpen);
13+
const intl = useIntl();
1014

1115
if (!isOpen || learner === null) {
1216
onClose();
1317
return null;
1418
}
1519

16-
return <div>Unenroll Modal</div>;
20+
return (
21+
<ModalDialog isOpen={isOpen} onClose={onClose} title={intl.formatMessage(messages.unenrollLearners)} isOverflowVisible={false}>
22+
<ModalDialog.Header>
23+
<h3 className="text-primary-500">{intl.formatMessage(messages.unenrollLearnerTitle)}</h3>
24+
</ModalDialog.Header>
25+
<ModalDialog.Body className="py-4">
26+
<p>{intl.formatMessage(messages.unenrollLearnersConfirmation, { name: learner.fullName })}</p>
27+
</ModalDialog.Body>
28+
<ModalDialog.Footer>
29+
<Button variant="tertiary" onClick={onClose}>{intl.formatMessage(messages.cancelButton)}</Button>
30+
<Button className="ml-2">{intl.formatMessage(messages.unenrollButton)}</Button>
31+
</ModalDialog.Footer>
32+
</ModalDialog>
33+
);
1734
};
1835

1936
export default UnenrollModal;

src/enrollments/data/api.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,23 @@ export const getEnrollmentStatus = async (
2626
);
2727
return camelCaseObject(data);
2828
};
29+
30+
export const enrollLearners = async (
31+
courseId: string,
32+
users: string[]
33+
): Promise<void> => {
34+
await getAuthenticatedHttpClient().post(
35+
`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/enrollments/`,
36+
{ users }
37+
);
38+
};
39+
40+
export const unenrollLearners = async (
41+
courseId: string,
42+
users: string[]
43+
): Promise<void> => {
44+
await getAuthenticatedHttpClient().delete(
45+
`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/enrollments/`,
46+
{ data: { users } }
47+
);
48+
};

src/enrollments/data/apiHook.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useQuery } from '@tanstack/react-query';
2-
import { getEnrollments, getEnrollmentStatus, PaginationParams } from './api';
1+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2+
import { enrollLearners, getEnrollments, getEnrollmentStatus, PaginationParams } from './api';
33
import { enrollmentsQueryKeys } from './queryKeys';
44

55
export const useEnrollments = (courseId: string, pagination: PaginationParams) => (
@@ -16,3 +16,13 @@ export const useEnrollmentByUserId = (courseId: string, userIdentifier: string)
1616
enabled: false,
1717
})
1818
);
19+
20+
export const useEnrollLearners = (courseId: string) => {
21+
const queryClient = useQueryClient();
22+
return (useMutation({
23+
mutationFn: (users: string[]) => enrollLearners(courseId, users),
24+
onSuccess: () => {
25+
queryClient.invalidateQueries({ queryKey: enrollmentsQueryKeys.byCourse(courseId) });
26+
},
27+
}));
28+
};

src/enrollments/messages.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const messages = defineMessages({
1414
enrollLearners: {
1515
id: 'instruct.enrollments.enrollLearners',
1616
defaultMessage: 'Enroll Learners',
17-
description: 'Button label for enrolling learners',
17+
description: 'Button label an modal title for enrolling learners',
1818
},
1919
checkEnrollmentStatus: {
2020
id: 'instruct.enrollments.checkEnrollmentStatus',
@@ -62,25 +62,60 @@ const messages = defineMessages({
6262
description: 'Label for true boolean value',
6363
},
6464
addLearnerInstructions: {
65-
id: 'instruct.enrollments.checkEnrollmentStatusModal.addLearnerInstructions',
65+
id: 'instruct.enrollments.modals.checkEnrollmentStatus.addLearnerInstructions',
6666
defaultMessage: 'Learner’s My Open edX email address or username',
6767
description: 'Instructions for enroll learners to the course',
6868
},
69-
enrollLearnersPlaceholder: {
70-
id: 'instruct.enrollments.checkEnrollmentStatusModal.enrollLearnersPlaceholder',
69+
enrollmentStatusPlaceholder: {
70+
id: 'instruct.enrollments.modals.checkEnrollmentStatus.enrollmentStatusPlaceholder',
7171
defaultMessage: 'Learner email address or username',
7272
description: 'Placeholder text for enrolling learners textarea',
7373
},
7474
closeButton: {
75-
id: 'instruct.enrollments.checkEnrollmentStatusModal.closeButton',
75+
id: 'instruct.enrollments.modals.closeButton',
7676
defaultMessage: 'Close',
7777
description: 'Label for close button in modals',
7878
},
7979
statusResponseMessage: {
80-
id: 'instruct.enrollments.checkEnrollmentStatusModal.statusResponseMessage',
80+
id: 'instruct.enrollments.modals.checkEnrollmentStatus.statusResponseMessage',
8181
defaultMessage: 'Enrollment status for {learnerIdentifier}: {status}',
8282
description: 'Message displaying the enrollment status for a learner',
83-
}
83+
},
84+
enrollLearnersPlaceholder: {
85+
id: 'instruct.enrollments.modals.enrollLearners.enrollLearnersPlaceholder',
86+
defaultMessage: 'Email addresses / Usernames',
87+
description: 'Placeholder text for enrolling learners textarea',
88+
},
89+
enrollLearnerInstructions: {
90+
id: 'instruct.enrollments.modals.enrollLearners.enrollLearnerInstructions',
91+
defaultMessage: 'Enter email addresses and/or usernames separated by new lines or commas. You will not get notification for emails that bounce, so please double-check spelling.',
92+
description: 'Instructions for enrolling learners to the course',
93+
},
94+
unenrollLearners: {
95+
id: 'instruct.enrollments.modals.unenrollLearners',
96+
defaultMessage: 'Unenroll Learners',
97+
description: 'Title for unenroll learners modal',
98+
},
99+
unenrollLearnersConfirmation: {
100+
id: 'instruct.enrollments.modals.unenrollLearnersConfirmation',
101+
defaultMessage: 'Unenroll {name} from course?',
102+
description: 'Confirmation message for unenrolling learners',
103+
},
104+
unenrollLearnerTitle: {
105+
id: 'instruct.enrollments.modals.unenrollLearnerTitle',
106+
defaultMessage: 'Unenroll Learner?',
107+
description: 'Title for unenroll learner modal',
108+
},
109+
saveButton: {
110+
id: 'instruct.enrollments.modals.saveButton',
111+
defaultMessage: 'Save',
112+
description: 'Label for save button in modals',
113+
},
114+
cancelButton: {
115+
id: 'instruct.enrollments.modals.cancelButton',
116+
defaultMessage: 'Cancel',
117+
description: 'Label for cancel button in modals',
118+
},
84119
});
85120

86121
export default messages;

0 commit comments

Comments
 (0)