Skip to content

Commit a83b4d5

Browse files
feat: add individual extension modal
1 parent 0aaed3b commit a83b4d5

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
@@ -14,6 +14,7 @@ jest.mock('react-router-dom', () => ({
1414
jest.mock('./data/apiHook', () => ({
1515
useDateExtensions: jest.fn(),
1616
useResetDateExtensionMutation: jest.fn(),
17+
useAddDateExtensionMutation: jest.fn(() => ({ mutate: jest.fn() })),
1718
}));
1819

1920
const mockDateExtensions = [

src/dateExtensions/DateExtensionsPage.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@ 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';
1010
import { useAlert } from '@src/providers/AlertProvider';
11+
import AddExtensionModal from './components/AddExtensionModal';
1112

1213
const DateExtensionsPage = () => {
1314
const intl = useIntl();
14-
const { courseId } = useParams<{ courseId: string }>();
15+
const { courseId = '' } = useParams<{ courseId: string }>();
1516
const { mutate: resetMutation } = useResetDateExtensionMutation();
17+
const { mutate: addExtensionMutation } = useAddDateExtensionMutation();
1618
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
1719
const [selectedUser, setSelectedUser] = useState<LearnerDateExtension | null>(null);
1820
const { showToast, showModal, removeAlert, clearAlerts } = useAlert();
21+
const [isAddExtensionModalOpen, setIsAddExtensionModalOpen] = useState(false);
1922

2023
const handleResetExtensions = (user: LearnerDateExtension) => {
2124
clearAlerts();
@@ -57,14 +60,36 @@ const DateExtensionsPage = () => {
5760
}
5861
};
5962

63+
const handleOpenAddExtension = () => {
64+
setIsAddExtensionModalOpen(true);
65+
};
66+
67+
const handleAddExtension = ({ email_or_username, block_id, due_datetime, reason }) => {
68+
addExtensionMutation({ courseId, extensionData: {
69+
email_or_username,
70+
block_id,
71+
due_datetime,
72+
reason
73+
} }, {
74+
onError: handleErrorOnReset,
75+
onSuccess: handleSuccessOnReset
76+
});
77+
};
78+
6079
return (
6180
<div className="mt-4.5 mb-4 mx-4">
6281
<h3>{intl.formatMessage(messages.dateExtensionsTitle)}</h3>
6382
<div className="d-flex align-items-center justify-content-between mb-3.5">
6483
<p>filters</p>
65-
<Button>+ {intl.formatMessage(messages.addIndividualExtension)}</Button>
84+
<Button onClick={handleOpenAddExtension}>+ {intl.formatMessage(messages.addIndividualExtension)}</Button>
6685
</div>
6786
<DateExtensionsList onResetExtensions={handleResetExtensions} />
87+
<AddExtensionModal
88+
isOpen={isAddExtensionModalOpen}
89+
title={intl.formatMessage(messages.addIndividualDueDateExtension)}
90+
onClose={() => setIsAddExtensionModalOpen(false)}
91+
onSubmit={handleAddExtension}
92+
/>
6893
<ResetExtensionsModal
6994
isOpen={isResetModalOpen}
7095
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: string, params: ResetDueDateP
2121
const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/courses/${courseId}/instructor/api/reset_due_date`, params);
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
import { ResetDueDateParams } from '../types';
55

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

src/dateExtensions/messages.ts

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

76111
export default messages;

0 commit comments

Comments
 (0)