Skip to content

Commit 1230699

Browse files
feat: add individual extension modal
1 parent 18c3e75 commit 1230699

File tree

7 files changed

+199
-4
lines changed

7 files changed

+199
-4
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Button, FormControl, FormGroup, FormLabel } from '@openedx/paragon';
2+
3+
interface SpecifyLearnerFieldProps {
4+
onChange: (value: string) => void,
5+
}
6+
7+
const SpecifyLearnerField = ({ onChange }: SpecifyLearnerFieldProps) => {
8+
return (
9+
<FormGroup size="sm">
10+
<FormLabel>Specify Learner:</FormLabel>
11+
<div className="d-flex">
12+
<FormControl className="mr-2" name="email_or_username" placeholder="Learner email, address or username" size="md" autoResize onChange={(e) => onChange(e.target.value)} />
13+
<Button>Select</Button>
14+
</div>
15+
</FormGroup>
16+
);
17+
};
18+
19+
export default SpecifyLearnerField;

src/dateExtensions/DateExtensionsPage.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useDateExtensions, useResetDateExtensionMutation } from './data/apiHook
88
jest.mock('./data/apiHook', () => ({
99
useDateExtensions: jest.fn(),
1010
useResetDateExtensionMutation: jest.fn(),
11+
useAddDateExtensionMutation: jest.fn(() => ({ mutate: jest.fn() })),
1112
}));
1213

1314
const mockDateExtensions = [

src/dateExtensions/DateExtensionsPage.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ import messages from './messages';
66
import DateExtensionsList from './components/DateExtensionsList';
77
import ResetExtensionsModal from './components/ResetExtensionsModal';
88
import { LearnerDateExtension } from './types';
9-
import { useResetDateExtensionMutation } from './data/apiHook';
9+
import { useAddDateExtensionMutation, useResetDateExtensionMutation } from './data/apiHook';
10+
import AddExtensionModal from './components/AddExtensionModal';
1011

1112
// const successMessage = 'Successfully reset due date for student Phu Nguyen for A subsection with two units (block-v1:SchemaAximWGU+WGU101+1+type@sequential+block@3984030755104708a86592cf23fb1ae4) to 2025-08-21 00:00';
1213

1314
const DateExtensionsPage = () => {
1415
const intl = useIntl();
15-
const { courseId } = useParams<{ courseId: string }>();
16+
const { courseId = '' } = useParams<{ courseId: string }>();
1617
const { mutate: resetMutation } = useResetDateExtensionMutation();
18+
const { mutate: addExtensionMutation } = useAddDateExtensionMutation();
1719
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
1820
const [selectedUser, setSelectedUser] = useState<LearnerDateExtension | null>(null);
1921
const [successMessage, setSuccessMessage] = useState<string>('');
2022
const [errorMessage, setErrorMessage] = useState<string>('');
23+
const [isAddExtensionModalOpen, setIsAddExtensionModalOpen] = useState(false);
2124

2225
const handleResetExtensions = (user: LearnerDateExtension) => {
2326
setIsResetModalOpen(true);
@@ -51,14 +54,36 @@ const DateExtensionsPage = () => {
5154
}
5255
};
5356

57+
const handleOpenAddExtension = () => {
58+
setIsAddExtensionModalOpen(true);
59+
};
60+
61+
const handleAddExtension = ({ email_or_username, block_id, due_datetime, reason }) => {
62+
addExtensionMutation({ courseId, extensionData: {
63+
email_or_username,
64+
block_id,
65+
due_datetime,
66+
reason
67+
} }, {
68+
onError: handleErrorOnReset,
69+
onSuccess: handleSuccessOnReset
70+
});
71+
};
72+
5473
return (
5574
<Container className="mt-4.5 mb-4 mx-4">
5675
<h3>{intl.formatMessage(messages.dateExtensionsTitle)}</h3>
5776
<div className="d-flex align-items-center justify-content-between mb-3.5">
5877
<p>filters</p>
59-
<Button>+ {intl.formatMessage(messages.addIndividualExtension)}</Button>
78+
<Button onClick={handleOpenAddExtension}>+ {intl.formatMessage(messages.addIndividualExtension)}</Button>
6079
</div>
6180
<DateExtensionsList onResetExtensions={handleResetExtensions} />
81+
<AddExtensionModal
82+
isOpen={isAddExtensionModalOpen}
83+
title={intl.formatMessage(messages.addIndividualDueDateExtension)}
84+
onClose={() => setIsAddExtensionModalOpen(false)}
85+
onSubmit={handleAddExtension}
86+
/>
6287
<ResetExtensionsModal
6388
isOpen={isResetModalOpen}
6489
message={intl.formatMessage(messages.resetConfirmationMessage)}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { ActionRow, Button, FormAutosuggest, FormAutosuggestOption, FormControl, FormGroup, FormLabel, ModalDialog } from '@openedx/paragon';
2+
import { useIntl } from '@openedx/frontend-base';
3+
import SpecifyLearnerField from '../../components/SpecifyLearnerField/SpecifyLearnerField';
4+
import messages from '../messages';
5+
6+
interface AddExtensionModalProps {
7+
isOpen: boolean,
8+
title: string,
9+
onClose: () => void,
10+
onSubmit: ({ email_or_username, block_id, due_datetime, reason }: {
11+
email_or_username: string,
12+
block_id: string,
13+
due_datetime: string,
14+
reason: string,
15+
}) => void,
16+
}
17+
18+
const AddExtensionModal = ({ isOpen, title, onClose, onSubmit }: AddExtensionModalProps) => {
19+
const intl = useIntl();
20+
21+
const options = [
22+
{ label: 'is an example', value: 'example' },
23+
{ label: 'another example', value: 'another' }
24+
];
25+
26+
const handleSubmit = () => {
27+
onSubmit({
28+
email_or_username: 'dianasalas',
29+
block_id: 'block-v1:DV-edtech+check+2025-05+type@sequential+block@a9500056bbb544ea82fad0d3957c6932',
30+
due_datetime: '2025-01-21 00:00:00',
31+
reason: 'Personal reasons'
32+
});
33+
};
34+
35+
return (
36+
<ModalDialog isOpen={isOpen} onClose={onClose} title={title} isOverflowVisible={false} size="xl">
37+
<ModalDialog.Header className="p-3 pl-4">
38+
<h3>{title}</h3>
39+
</ModalDialog.Header>
40+
<ModalDialog.Body className="border-bottom border-top">
41+
<div className="pt-3">
42+
<p>{intl.formatMessage(messages.extensionInstructions)}</p>
43+
<FormGroup size="sm">
44+
<div className="container-fluid border-bottom mb-4.5 pb-3">
45+
<div className="row">
46+
<div className="col-sm-12 col-md-6">
47+
<SpecifyLearnerField onChange={() => {}} />
48+
</div>
49+
<div className="col-sm-12 col-md-4">
50+
<FormLabel>{intl.formatMessage(messages.selectGradedSubsection)}</FormLabel>
51+
<FormAutosuggest placeholder={intl.formatMessage(messages.selectGradedSubsection)} name="block_id">
52+
{
53+
options.map((option) => (
54+
<FormAutosuggestOption key={option.value} value={option.value} onChange={() => {}}>
55+
{option.label}
56+
</FormAutosuggestOption>
57+
))
58+
}
59+
</FormAutosuggest>
60+
</div>
61+
</div>
62+
</div>
63+
<div>
64+
<h4>{intl.formatMessage(messages.defineExtension)}</h4>
65+
<FormLabel>
66+
{intl.formatMessage(messages.extensionDate)}:
67+
</FormLabel>
68+
<div className="d-md-flex w-md-50 align-items-center">
69+
<FormControl name="due_date" type="date" size="md" />
70+
<FormControl name="due_time" type="time" size="md" className="mt-sm-3 mt-md-0" />
71+
</div>
72+
<div className="mt-3">
73+
<FormLabel>
74+
{intl.formatMessage(messages.reasonForExtension)}:
75+
</FormLabel>
76+
<FormControl name="reason" placeholder="Reason for extension" size="md" />
77+
</div>
78+
</div>
79+
</FormGroup>
80+
</div>
81+
</ModalDialog.Body>
82+
<ModalDialog.Footer className="p-4">
83+
<ActionRow>
84+
<Button variant="tertiary" onClick={onClose}>{intl.formatMessage(messages.cancel)}</Button>
85+
<Button onClick={handleSubmit}>{intl.formatMessage(messages.addExtension)}</Button>
86+
</ActionRow>
87+
</ModalDialog.Footer>
88+
</ModalDialog>
89+
);
90+
};
91+
92+
export default AddExtensionModal;

src/dateExtensions/data/api.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,15 @@ export const resetDateExtension = async (courseId, userId) => {
2121
const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/api/instructor/v1/courses/${courseId}/date-extensions/${userId}/reset`);
2222
return camelCaseObject(data);
2323
};
24+
25+
interface AddDateExtensionParams {
26+
email_or_username: string,
27+
block_id: string,
28+
due_datetime: string,
29+
reason: string,
30+
}
31+
32+
export const addDateExtension = async (courseId, extensionData: AddDateExtensionParams) => {
33+
const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/change_due_date`, extensionData);
34+
return camelCaseObject(data);
35+
};

src/dateExtensions/data/apiHook.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2-
import { getDateExtensions, PaginationQueryKeys, resetDateExtension } from './api';
2+
import { getDateExtensions, PaginationQueryKeys, resetDateExtension, addDateExtension } from './api';
33
import { dateExtensionsQueryKeys } from './queryKeys';
44

55
export const useDateExtensions = (courseId: string, pagination: PaginationQueryKeys) => (
@@ -19,3 +19,14 @@ export const useResetDateExtensionMutation = () => {
1919
},
2020
});
2121
};
22+
23+
export const useAddDateExtensionMutation = () => {
24+
const queryClient = useQueryClient();
25+
return useMutation({
26+
mutationFn: ({ courseId, extensionData }: { courseId: string, extensionData: any }) =>
27+
addDateExtension(courseId, extensionData),
28+
onSuccess: ({ courseId }) => {
29+
queryClient.invalidateQueries({ queryKey: dateExtensionsQueryKeys.byCourse(courseId) });
30+
},
31+
});
32+
};

src/dateExtensions/messages.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,41 @@ const messages = defineMessages({
6666
defaultMessage: 'Close',
6767
description: 'Label for the close button in the reset modal',
6868
},
69+
addIndividualDueDateExtension: {
70+
id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.title',
71+
defaultMessage: 'Add Individual Due Date Extension',
72+
description: 'Title for the add individual due date extension modal',
73+
},
74+
addExtension: {
75+
id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.addExtension',
76+
defaultMessage: 'Add Extension',
77+
description: 'Label for the add extension button',
78+
},
79+
extensionInstructions: {
80+
id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.extensionInstructions',
81+
defaultMessage: 'To grant an extension, select a student, graded subsection, and define the extension due date and time.',
82+
description: 'Instructions for adding an individual due date extension',
83+
},
84+
defineExtension: {
85+
id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.defineExtension',
86+
defaultMessage: 'Define Extension',
87+
description: 'Label for the define extension section',
88+
},
89+
extensionDate: {
90+
id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.extensionDate',
91+
defaultMessage: 'Extension Date',
92+
description: 'Label for the extension date field',
93+
},
94+
reasonForExtension: {
95+
id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.reasonForExtension',
96+
defaultMessage: 'Reason for Extension',
97+
description: 'Label for the reason for extension field',
98+
},
99+
selectGradedSubsection: {
100+
id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.selectGradedSubsection',
101+
defaultMessage: 'Select Graded Subsection',
102+
description: 'Label for the select graded subsection field',
103+
},
69104
});
70105

71106
export default messages;

0 commit comments

Comments
 (0)