Skip to content

Commit 0a6d816

Browse files
feat: reset extensions modal
1 parent 1d16948 commit 0a6d816

File tree

6 files changed

+143
-5
lines changed

6 files changed

+143
-5
lines changed

src/data/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ export const getDateExtensions = async (
2929
);
3030
return camelCaseObject(data);
3131
};
32+
33+
export const resetDateExtension = async (courseId, userId) => {
34+
const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/api/instructor/v1/courses/${courseId}/date-extensions/${userId}/reset`);
35+
return camelCaseObject(data);
36+
};

src/data/apiHook.ts

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

55
const COURSE_INFO_QUERY_KEY = ['courseInfo'];
@@ -23,3 +23,10 @@ export const useDateExtensions = (courseId: string, pagination: PaginationQueryK
2323
queryFn: () => getDateExtensions(courseId, pagination),
2424
})
2525
);
26+
27+
export const useResetDateExtensionMutation = () => {
28+
return useMutation({
29+
mutationFn: ({ courseId, userId }: { courseId: string, userId: number }) =>
30+
resetDateExtension(courseId, userId),
31+
});
32+
};

src/dateExtensions/DateExtensionsPage.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,35 @@
1+
import { useState } from 'react';
12
import { useIntl } from '@openedx/frontend-base';
3+
import { Button, Container } from '@openedx/paragon';
24
import messages from './messages';
35
import DateExtensionsList from './components/DateExtensionsList';
4-
import { Button, Container } from '@openedx/paragon';
6+
import ResetExtensionsModal from './components/ResetExtensionsModal';
57
import { LearnerDateExtension } from './types';
68

79
// 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';
810

911
const DateExtensionsPage = () => {
1012
const intl = useIntl();
13+
const [isModalOpen, setIsModalOpen] = useState(false);
14+
const [selectedUser, setSelectedUser] = useState<LearnerDateExtension | null>(null);
1115

1216
const handleResetExtensions = (user: LearnerDateExtension) => {
13-
// Implementation for resetting extensions will go here
14-
console.log(user);
17+
setIsModalOpen(true);
18+
setSelectedUser(user);
19+
};
20+
21+
const handleConfirmReset = () => {
22+
if (selectedUser) {
23+
// Call the API to reset the extensions for the selected user
24+
console.log(`Resetting extensions for user: ${selectedUser.username}`);
25+
}
26+
setIsModalOpen(false);
27+
setSelectedUser(null);
28+
};
29+
30+
const handleCancelReset = () => {
31+
setIsModalOpen(false);
32+
setSelectedUser(null);
1533
};
1634

1735
return (
@@ -22,6 +40,14 @@ const DateExtensionsPage = () => {
2240
<Button>+ {intl.formatMessage(messages.addIndividualExtension)}</Button>
2341
</div>
2442
<DateExtensionsList onResetExtensions={handleResetExtensions} />
43+
<ResetExtensionsModal
44+
isOpen={isModalOpen}
45+
message={intl.formatMessage(messages.resetConfirmationMessage)}
46+
title={intl.formatMessage(messages.resetConfirmationHeader, { username: selectedUser?.username })}
47+
onCancelReset={handleCancelReset}
48+
onClose={handleCancelReset}
49+
onConfirmReset={handleConfirmReset}
50+
/>
2551
</Container>
2652
);
2753
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import ResetExtensionsModal from './ResetExtensionsModal';
4+
import { renderWithIntl } from '../../testUtils';
5+
import messages from '../messages';
6+
7+
describe('ResetExtensionsModal', () => {
8+
const defaultProps = {
9+
isOpen: true,
10+
message: 'Test message',
11+
title: 'Test title',
12+
onCancelReset: jest.fn(),
13+
onClose: jest.fn(),
14+
onConfirmReset: jest.fn(),
15+
};
16+
17+
const renderModal = (props = {}) => renderWithIntl(
18+
<ResetExtensionsModal {...defaultProps} {...props} />
19+
);
20+
21+
it('renders modal with correct title and message', () => {
22+
renderModal();
23+
expect(screen.getByText('Test title')).toBeInTheDocument();
24+
expect(screen.getByText('Test message')).toBeInTheDocument();
25+
});
26+
27+
it('calls onCancelReset when cancel button is clicked', async () => {
28+
const user = userEvent.setup();
29+
renderModal();
30+
await user.click(screen.getByRole('button', { name: messages.cancel.defaultMessage }));
31+
expect(defaultProps.onCancelReset).toHaveBeenCalled();
32+
});
33+
34+
it('calls onConfirmReset when confirm button is clicked', async () => {
35+
const user = userEvent.setup();
36+
renderModal();
37+
await user.click(screen.getByRole('button', { name: messages.confirm.defaultMessage }));
38+
expect(defaultProps.onConfirmReset).toHaveBeenCalled();
39+
});
40+
41+
it('does not render when isOpen is false', () => {
42+
renderModal({ isOpen: false });
43+
expect(screen.queryByText('Test title')).not.toBeInTheDocument();
44+
});
45+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useIntl } from '@openedx/frontend-base';
2+
import { ModalDialog, ActionRow, Button } from '@openedx/paragon';
3+
import messages from '../messages';
4+
5+
interface ResetExtensionsModalProps {
6+
isOpen: boolean,
7+
message: string,
8+
title: string,
9+
onCancelReset: () => void,
10+
onClose: () => void,
11+
onConfirmReset: () => void,
12+
}
13+
14+
const ResetExtensionsModal = ({
15+
isOpen,
16+
message,
17+
title,
18+
onCancelReset,
19+
onClose,
20+
onConfirmReset,
21+
}: ResetExtensionsModalProps) => {
22+
const intl = useIntl();
23+
return (
24+
<ModalDialog isOpen={isOpen} onClose={onClose} hasCloseButton={false} title={title} isOverflowVisible={false} className="p-4">
25+
<h4 className="mb-2.5">{title}</h4>
26+
<p className="mb-2.5">{message}</p>
27+
<ActionRow>
28+
<Button variant="tertiary" onClick={onCancelReset}>{intl.formatMessage(messages.cancel)}</Button>
29+
<Button variant="primary" onClick={onConfirmReset}>{intl.formatMessage(messages.confirm)}</Button>
30+
</ActionRow>
31+
</ModalDialog>
32+
);
33+
};
34+
35+
export default ResetExtensionsModal;

src/dateExtensions/messages.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,26 @@ const messages = defineMessages({
4141
defaultMessage: 'Reset',
4242
description: 'Label for the reset column in the date extensions table',
4343
},
44+
resetConfirmationHeader: {
45+
id: 'instruct.dateExtensions.page.resetModal.confirmationHeader',
46+
defaultMessage: 'Reset extensions for {username}?',
47+
description: 'Header for the reset confirmation modal',
48+
},
49+
resetConfirmationMessage: {
50+
id: 'instruct.dateExtensions.page.resetModal.confirmationMessage',
51+
defaultMessage: 'Resetting a problem\'s due date rescinds a due date extension for a student on a particular subsection. This will revert the due date for the student back to the problem\'s original due date.',
52+
description: 'Confirmation message for resetting extensions in the reset modal',
53+
},
54+
cancel: {
55+
id: 'instruct.dateExtensions.page.resetModal.cancel',
56+
defaultMessage: 'Cancel',
57+
description: 'Label for the cancel button in the reset modal',
58+
},
59+
confirm: {
60+
id: 'instruct.dateExtensions.page.resetModal.confirm',
61+
defaultMessage: 'Reset Due Date for Student',
62+
description: 'Label for the confirm button in the reset modal',
63+
},
4464
});
4565

4666
export default messages;

0 commit comments

Comments
 (0)