Skip to content

Commit bf28b2a

Browse files
feat: add individual extension modal
1 parent 5e0f65a commit bf28b2a

File tree

9 files changed

+212
-5
lines changed

9 files changed

+212
-5
lines changed

src/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const app: App = {
1111
slots: [],
1212
config: {
1313
NODE_ENV: 'development',
14-
LMS_BASE_URL: 'http://localhost:18000'
14+
LMS_BASE_URL: 'http://local.openedx.io:8000'
1515
}
1616
};
1717

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" 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/data/api.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,15 @@ export const resetDateExtension = async (courseId, userId) => {
3434
const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/api/instructor/v1/courses/${courseId}/date-extensions/${userId}/reset`);
3535
return camelCaseObject(data);
3636
};
37+
38+
interface AddDateExtensionParams {
39+
student: string,
40+
url: string,
41+
due_datetime: string,
42+
reason: string,
43+
}
44+
45+
export const addDateExtension = async (courseId, extensionData: AddDateExtensionParams) => {
46+
const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/courses/${courseId}/instructor/api/change_due_date`, extensionData);
47+
return camelCaseObject(data);
48+
};

src/data/apiHook.ts

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

55
const COURSE_INFO_QUERY_KEY = ['courseInfo'];
@@ -34,3 +34,14 @@ export const useResetDateExtensionMutation = () => {
3434
},
3535
});
3636
};
37+
38+
export const useAddDateExtensionMutation = () => {
39+
const queryClient = useQueryClient();
40+
return useMutation({
41+
mutationFn: ({ courseId, extensionData }: { courseId: string, extensionData: any }) =>
42+
addDateExtension(courseId, extensionData),
43+
onSuccess: ({ courseId }) => {
44+
queryClient.invalidateQueries({ queryKey: dateExtensionsQueryKeys.byCourse(courseId) });
45+
},
46+
});
47+
};

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/apiHoo
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 = ({ student, url, due_datetime, reason }) => {
62+
addExtensionMutation({ courseId, extensionData: {
63+
student,
64+
url,
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" fluid="xl">
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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { ActionRow, Button, FormControl, FormGroup, FormLabel, ModalDialog } from '@openedx/paragon';
2+
import { useIntl } from '@openedx/frontend-base';
3+
import GradedSubsectionField from './GradedSubsectionField';
4+
import SpecifyLearnerField from '../../components/SpecifyLearnerField/SpecifyLearnerField';
5+
import messages from '../messages';
6+
7+
interface AddExtensionModalProps {
8+
isOpen: boolean,
9+
title: string,
10+
onClose: () => void,
11+
onSubmit: ({ student, url, due_datetime, reason }: {
12+
student: string,
13+
url: string,
14+
due_datetime: string,
15+
reason: string,
16+
}) => void,
17+
}
18+
19+
const AddExtensionModal = ({ isOpen, title, onClose, onSubmit }: AddExtensionModalProps) => {
20+
const intl = useIntl();
21+
22+
const handleSubmit = () => {
23+
onSubmit({
24+
student: 'dianasalas',
25+
url: 'block-v1:DV-edtech+check+2025-05+type@sequential+block@a9500056bbb544ea82fad0d3957c6932',
26+
due_datetime: '2025-01-21 00:00:00',
27+
reason: 'Personal reasons'
28+
});
29+
};
30+
31+
return (
32+
<ModalDialog isOpen={isOpen} onClose={onClose} title={title} isOverflowVisible={false} size="xl">
33+
<ModalDialog.Header className="p-3 pl-4">
34+
<h3>{title}</h3>
35+
</ModalDialog.Header>
36+
<ModalDialog.Body className="border-bottom border-top">
37+
<div className="pt-3">
38+
<p>{intl.formatMessage(messages.extensionInstructions)}</p>
39+
<FormGroup size="sm">
40+
<div className="container-fluid border-bottom mb-4.5 pb-3">
41+
<div className="row">
42+
<div className="col-sm-12 col-md-6">
43+
<SpecifyLearnerField onChange={() => {}} />
44+
</div>
45+
<div className="col-sm-12 col-md-4">
46+
<GradedSubsectionField onChange={() => {}} />
47+
</div>
48+
</div>
49+
</div>
50+
<div>
51+
<h4>{intl.formatMessage(messages.defineExtension)}</h4>
52+
<FormLabel>
53+
{intl.formatMessage(messages.extensionDate)}:
54+
</FormLabel>
55+
<div className="d-md-flex w-md-50 align-items-center">
56+
<FormControl type="date" size="md" />
57+
<FormControl type="time" size="md" className="mt-sm-3 mt-md-0" />
58+
</div>
59+
<div className="mt-3">
60+
<FormLabel>
61+
{intl.formatMessage(messages.reasonForExtension)}:
62+
</FormLabel>
63+
<FormControl placeholder="Reason for extension" size="md" />
64+
</div>
65+
</div>
66+
</FormGroup>
67+
</div>
68+
</ModalDialog.Body>
69+
<ModalDialog.Footer className="p-4">
70+
<ActionRow>
71+
<Button variant="tertiary" onClick={onClose}>{intl.formatMessage(messages.cancel)}</Button>
72+
<Button onClick={handleSubmit}>{intl.formatMessage(messages.addExtension)}</Button>
73+
</ActionRow>
74+
</ModalDialog.Footer>
75+
</ModalDialog>
76+
);
77+
};
78+
79+
export default AddExtensionModal;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { FormAutosuggest, FormAutosuggestOption, FormGroup, FormLabel } from '@openedx/paragon';
2+
3+
const options = [
4+
{ label: 'is an example', value: 'example' },
5+
{ label: 'another example', value: 'another' }
6+
];
7+
8+
interface GradedSubsectionFieldProps {
9+
onChange: (value: string) => void,
10+
}
11+
12+
const GradedSubsectionField = ({ onChange }: GradedSubsectionFieldProps) => {
13+
return (
14+
<FormGroup size="sm">
15+
<FormLabel>Select Graded Subsection:</FormLabel>
16+
<FormAutosuggest placeholder="Select Graded Subsection">
17+
{
18+
options.map((option) => (
19+
<FormAutosuggestOption key={option.value} value={option.value} onChange={onChange}>
20+
{option.label}
21+
</FormAutosuggestOption>
22+
))
23+
}
24+
</FormAutosuggest>
25+
</FormGroup>
26+
);
27+
};
28+
29+
export default GradedSubsectionField;

src/dateExtensions/messages.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,37 @@ 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+
69100
});
70101

71102
export default messages;

0 commit comments

Comments
 (0)