From 613b7a5dd376790c355d5f8b0101f7f1fbd1669b Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Thu, 6 Nov 2025 19:52:56 -0600 Subject: [PATCH 1/9] feat: reset extensions modal --- src/dateExtensions/DateExtensionsPage.tsx | 32 +++++++++++-- .../components/ResetExtensionsModal.test.tsx | 45 +++++++++++++++++++ .../components/ResetExtensionsModal.tsx | 35 +++++++++++++++ src/dateExtensions/data/api.ts | 5 +++ src/dateExtensions/data/apiHook.ts | 11 ++++- src/dateExtensions/messages.ts | 22 ++++++++- 6 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 src/dateExtensions/components/ResetExtensionsModal.test.tsx create mode 100644 src/dateExtensions/components/ResetExtensionsModal.tsx diff --git a/src/dateExtensions/DateExtensionsPage.tsx b/src/dateExtensions/DateExtensionsPage.tsx index 8c4807b5..549562b2 100644 --- a/src/dateExtensions/DateExtensionsPage.tsx +++ b/src/dateExtensions/DateExtensionsPage.tsx @@ -1,17 +1,35 @@ +import { useState } from 'react'; import { useIntl } from '@openedx/frontend-base'; +import { Button } from '@openedx/paragon'; import messages from './messages'; import DateExtensionsList from './components/DateExtensionsList'; -import { Button } from '@openedx/paragon'; +import ResetExtensionsModal from './components/ResetExtensionsModal'; import { LearnerDateExtension } from './types'; // 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'; const DateExtensionsPage = () => { const intl = useIntl(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); const handleResetExtensions = (user: LearnerDateExtension) => { - // Implementation for resetting extensions will go here - console.log(user); + setIsModalOpen(true); + setSelectedUser(user); + }; + + const handleConfirmReset = () => { + if (selectedUser) { + // Call the API to reset the extensions for the selected user + console.log(`Resetting extensions for user: ${selectedUser.username}`); + } + setIsModalOpen(false); + setSelectedUser(null); + }; + + const handleCancelReset = () => { + setIsModalOpen(false); + setSelectedUser(null); }; return ( @@ -22,6 +40,14 @@ const DateExtensionsPage = () => { + ); }; diff --git a/src/dateExtensions/components/ResetExtensionsModal.test.tsx b/src/dateExtensions/components/ResetExtensionsModal.test.tsx new file mode 100644 index 00000000..a03397d0 --- /dev/null +++ b/src/dateExtensions/components/ResetExtensionsModal.test.tsx @@ -0,0 +1,45 @@ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ResetExtensionsModal from './ResetExtensionsModal'; +import { renderWithIntl } from '../../testUtils'; +import messages from '../messages'; + +describe('ResetExtensionsModal', () => { + const defaultProps = { + isOpen: true, + message: 'Test message', + title: 'Test title', + onCancelReset: jest.fn(), + onClose: jest.fn(), + onConfirmReset: jest.fn(), + }; + + const renderModal = (props = {}) => renderWithIntl( + + ); + + it('renders modal with correct title and message', () => { + renderModal(); + expect(screen.getByText('Test title')).toBeInTheDocument(); + expect(screen.getByText('Test message')).toBeInTheDocument(); + }); + + it('calls onCancelReset when cancel button is clicked', async () => { + const user = userEvent.setup(); + renderModal(); + await user.click(screen.getByRole('button', { name: messages.cancel.defaultMessage })); + expect(defaultProps.onCancelReset).toHaveBeenCalled(); + }); + + it('calls onConfirmReset when confirm button is clicked', async () => { + const user = userEvent.setup(); + renderModal(); + await user.click(screen.getByRole('button', { name: messages.confirm.defaultMessage })); + expect(defaultProps.onConfirmReset).toHaveBeenCalled(); + }); + + it('does not render when isOpen is false', () => { + renderModal({ isOpen: false }); + expect(screen.queryByText('Test title')).not.toBeInTheDocument(); + }); +}); diff --git a/src/dateExtensions/components/ResetExtensionsModal.tsx b/src/dateExtensions/components/ResetExtensionsModal.tsx new file mode 100644 index 00000000..0fe4edd3 --- /dev/null +++ b/src/dateExtensions/components/ResetExtensionsModal.tsx @@ -0,0 +1,35 @@ +import { useIntl } from '@openedx/frontend-base'; +import { ModalDialog, ActionRow, Button } from '@openedx/paragon'; +import messages from '../messages'; + +interface ResetExtensionsModalProps { + isOpen: boolean, + message: string, + title: string, + onCancelReset: () => void, + onClose: () => void, + onConfirmReset: () => void, +} + +const ResetExtensionsModal = ({ + isOpen, + message, + title, + onCancelReset, + onClose, + onConfirmReset, +}: ResetExtensionsModalProps) => { + const intl = useIntl(); + return ( + +

{title}

+

{message}

+ + + + +
+ ); +}; + +export default ResetExtensionsModal; diff --git a/src/dateExtensions/data/api.ts b/src/dateExtensions/data/api.ts index 4e4afc5e..4be982b8 100644 --- a/src/dateExtensions/data/api.ts +++ b/src/dateExtensions/data/api.ts @@ -16,3 +16,8 @@ export const getDateExtensions = async ( ); return camelCaseObject(data); }; + +export const resetDateExtension = async (courseId, userId) => { + const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/api/instructor/v1/courses/${courseId}/date-extensions/${userId}/reset`); + return camelCaseObject(data); +}; diff --git a/src/dateExtensions/data/apiHook.ts b/src/dateExtensions/data/apiHook.ts index e0a42a04..1e1437b5 100644 --- a/src/dateExtensions/data/apiHook.ts +++ b/src/dateExtensions/data/apiHook.ts @@ -1,5 +1,5 @@ -import { useQuery } from '@tanstack/react-query'; -import { getDateExtensions, PaginationQueryKeys } from './api'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { getDateExtensions, PaginationQueryKeys, resetDateExtension } from './api'; import { dateExtensionsQueryKeys } from './queryKeys'; export const useDateExtensions = (courseId: string, pagination: PaginationQueryKeys) => ( @@ -8,3 +8,10 @@ export const useDateExtensions = (courseId: string, pagination: PaginationQueryK queryFn: () => getDateExtensions(courseId, pagination), }) ); + +export const useResetDateExtensionMutation = () => { + return useMutation({ + mutationFn: ({ courseId, userId }: { courseId: string, userId: number }) => + resetDateExtension(courseId, userId), + }); +}; diff --git a/src/dateExtensions/messages.ts b/src/dateExtensions/messages.ts index 6adc62cc..6c58b15d 100644 --- a/src/dateExtensions/messages.ts +++ b/src/dateExtensions/messages.ts @@ -45,7 +45,27 @@ const messages = defineMessages({ id: 'instruct.dateExtensions.page.button.resetExtensions', defaultMessage: 'Reset Extensions', description: 'Button text for resetting date extensions for a user', - } + }, + resetConfirmationHeader: { + id: 'instruct.dateExtensions.page.resetModal.confirmationHeader', + defaultMessage: 'Reset extensions for {username}?', + description: 'Header for the reset confirmation modal', + }, + resetConfirmationMessage: { + id: 'instruct.dateExtensions.page.resetModal.confirmationMessage', + 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.', + description: 'Confirmation message for resetting extensions in the reset modal', + }, + cancel: { + id: 'instruct.dateExtensions.page.resetModal.cancel', + defaultMessage: 'Cancel', + description: 'Label for the cancel button in the reset modal', + }, + confirm: { + id: 'instruct.dateExtensions.page.resetModal.confirm', + defaultMessage: 'Reset Due Date for Student', + description: 'Label for the confirm button in the reset modal', + }, }); export default messages; From 3316195be616d4d9c6db885a774c9a4a08e11bc6 Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Mon, 10 Nov 2025 12:43:27 -0600 Subject: [PATCH 2/9] feat: success message toast --- src/dateExtensions/DateExtensionsPage.tsx | 47 +++++++++++++++++------ src/dateExtensions/data/apiHook.ts | 6 ++- src/dateExtensions/messages.ts | 5 +++ 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/dateExtensions/DateExtensionsPage.tsx b/src/dateExtensions/DateExtensionsPage.tsx index 549562b2..a6c2f7bc 100644 --- a/src/dateExtensions/DateExtensionsPage.tsx +++ b/src/dateExtensions/DateExtensionsPage.tsx @@ -1,35 +1,54 @@ import { useState } from 'react'; +import { useParams } from 'react-router-dom'; import { useIntl } from '@openedx/frontend-base'; -import { Button } from '@openedx/paragon'; +import { AlertModal, Button, Toast } from '@openedx/paragon'; import messages from './messages'; import DateExtensionsList from './components/DateExtensionsList'; import ResetExtensionsModal from './components/ResetExtensionsModal'; import { LearnerDateExtension } from './types'; +import { useResetDateExtensionMutation } from './data/apiHook'; // 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'; const DateExtensionsPage = () => { const intl = useIntl(); + const { courseId } = useParams<{ courseId: string }>(); + const { mutate: resetMutation } = useResetDateExtensionMutation(); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); + const [successMessage, setSuccessMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); const handleResetExtensions = (user: LearnerDateExtension) => { setIsModalOpen(true); setSelectedUser(user); }; - const handleConfirmReset = () => { - if (selectedUser) { - // Call the API to reset the extensions for the selected user - console.log(`Resetting extensions for user: ${selectedUser.username}`); - } + const handleCloseModal = () => { setIsModalOpen(false); setSelectedUser(null); }; - const handleCancelReset = () => { - setIsModalOpen(false); - setSelectedUser(null); + const handleErrorOnReset = (error: any) => { + setErrorMessage(error.message); + }; + + const handleSuccessOnReset = (response: any) => { + const { message } = response; + setSuccessMessage(message); + handleCloseModal(); + }; + + const handleConfirmReset = async () => { + if (selectedUser && courseId) { + resetMutation({ + courseId, + userId: selectedUser.id + }, { + onError: handleErrorOnReset, + onSuccess: handleSuccessOnReset + }); + } }; return ( @@ -44,10 +63,16 @@ const DateExtensionsPage = () => { isOpen={isModalOpen} message={intl.formatMessage(messages.resetConfirmationMessage)} title={intl.formatMessage(messages.resetConfirmationHeader, { username: selectedUser?.username })} - onCancelReset={handleCancelReset} - onClose={handleCancelReset} + onCancelReset={handleCloseModal} + onClose={handleCloseModal} onConfirmReset={handleConfirmReset} /> + {}} className="text-break"> + {successMessage} + + setErrorMessage('')}>{intl.formatMessage(messages.close)}}> + {errorMessage} + ); }; diff --git a/src/dateExtensions/data/apiHook.ts b/src/dateExtensions/data/apiHook.ts index 1e1437b5..3e51560b 100644 --- a/src/dateExtensions/data/apiHook.ts +++ b/src/dateExtensions/data/apiHook.ts @@ -1,4 +1,4 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { getDateExtensions, PaginationQueryKeys, resetDateExtension } from './api'; import { dateExtensionsQueryKeys } from './queryKeys'; @@ -10,8 +10,12 @@ export const useDateExtensions = (courseId: string, pagination: PaginationQueryK ); export const useResetDateExtensionMutation = () => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ courseId, userId }: { courseId: string, userId: number }) => resetDateExtension(courseId, userId), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: dateExtensionsQueryKeys.all }); + }, }); }; diff --git a/src/dateExtensions/messages.ts b/src/dateExtensions/messages.ts index 6c58b15d..0b3db230 100644 --- a/src/dateExtensions/messages.ts +++ b/src/dateExtensions/messages.ts @@ -66,6 +66,11 @@ const messages = defineMessages({ defaultMessage: 'Reset Due Date for Student', description: 'Label for the confirm button in the reset modal', }, + close: { + id: 'instruct.dateExtensions.page.resetModal.close', + defaultMessage: 'Close', + description: 'Label for the close button in the reset modal', + }, }); export default messages; From 6791d3960627ffb144c7b8ad496a9946e41342b7 Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Mon, 10 Nov 2025 13:10:00 -0600 Subject: [PATCH 3/9] test: edit unit tests --- .../DateExtensionsPage.test.tsx | 40 ++++++++++++++++++- src/dateExtensions/DateExtensionsPage.tsx | 10 ++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/dateExtensions/DateExtensionsPage.test.tsx b/src/dateExtensions/DateExtensionsPage.test.tsx index 5bdc56a2..9af509f7 100644 --- a/src/dateExtensions/DateExtensionsPage.test.tsx +++ b/src/dateExtensions/DateExtensionsPage.test.tsx @@ -1,11 +1,13 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { IntlProvider } from '@openedx/frontend-base'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import DateExtensionsPage from './DateExtensionsPage'; -import { useDateExtensions } from './data/apiHook'; +import { useDateExtensions, useResetDateExtensionMutation } from './data/apiHook'; jest.mock('./data/apiHook', () => ({ useDateExtensions: jest.fn(), + useResetDateExtensionMutation: jest.fn(), })); const mockDateExtensions = [ @@ -19,12 +21,17 @@ const mockDateExtensions = [ }, ]; +const mutateMock = jest.fn(); + describe('DateExtensionsPage', () => { beforeEach(() => { (useDateExtensions as jest.Mock).mockReturnValue({ data: { count: mockDateExtensions.length, results: mockDateExtensions }, isLoading: false, }); + (useResetDateExtensionMutation as jest.Mock).mockReturnValue({ + mutate: mutateMock, + }); }); const RenderWithRouter = () => ( @@ -67,4 +74,35 @@ describe('DateExtensionsPage', () => { const resetLinks = screen.getAllByRole('button', { name: 'Reset Extensions' }); expect(resetLinks).toHaveLength(mockDateExtensions.length); }); + + it('opens reset modal when reset button is clicked', async () => { + render(); + const user = userEvent.setup(); + const resetButton = screen.getByRole('button', { name: 'Reset Extensions' }); + await user.click(resetButton); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText(/reset extensions for/i)).toBeInTheDocument(); + const confirmButton = screen.getByRole('button', { name: /reset due date/i }); + expect(confirmButton).toBeInTheDocument(); + }); + + it('calls reset mutation when confirm reset is clicked', async () => { + render(); + const user = userEvent.setup(); + const resetButton = screen.getByRole('button', { name: 'Reset Extensions' }); + await user.click(resetButton); + const confirmButton = screen.getByRole('button', { name: /reset due date/i }); + await user.click(confirmButton); + expect(mutateMock).toHaveBeenCalled(); + }); + + it('closes reset modal when cancel is clicked', async () => { + render(); + const user = userEvent.setup(); + const resetButton = screen.getByRole('button', { name: 'Reset Extensions' }); + await user.click(resetButton); + const cancelButton = screen.getByRole('button', { name: /cancel/i }); + await user.click(cancelButton); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); }); diff --git a/src/dateExtensions/DateExtensionsPage.tsx b/src/dateExtensions/DateExtensionsPage.tsx index a6c2f7bc..5c6c033d 100644 --- a/src/dateExtensions/DateExtensionsPage.tsx +++ b/src/dateExtensions/DateExtensionsPage.tsx @@ -14,18 +14,18 @@ const DateExtensionsPage = () => { const intl = useIntl(); const { courseId } = useParams<{ courseId: string }>(); const { mutate: resetMutation } = useResetDateExtensionMutation(); - const [isModalOpen, setIsModalOpen] = useState(false); + const [isResetModalOpen, setIsResetModalOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [successMessage, setSuccessMessage] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const handleResetExtensions = (user: LearnerDateExtension) => { - setIsModalOpen(true); + setIsResetModalOpen(true); setSelectedUser(user); }; const handleCloseModal = () => { - setIsModalOpen(false); + setIsResetModalOpen(false); setSelectedUser(null); }; @@ -60,7 +60,7 @@ const DateExtensionsPage = () => { { {}} className="text-break"> {successMessage} - setErrorMessage('')}>{intl.formatMessage(messages.close)}}> + setErrorMessage('')}>{intl.formatMessage(messages.close)}}> {errorMessage} From 73dce3d19e2505f19494e8891cab8670ef074041 Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Fri, 14 Nov 2025 11:16:40 -0600 Subject: [PATCH 4/9] chore: improving querykeys --- src/dateExtensions/data/apiHook.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dateExtensions/data/apiHook.ts b/src/dateExtensions/data/apiHook.ts index 3e51560b..6cbfefa9 100644 --- a/src/dateExtensions/data/apiHook.ts +++ b/src/dateExtensions/data/apiHook.ts @@ -14,8 +14,8 @@ export const useResetDateExtensionMutation = () => { return useMutation({ mutationFn: ({ courseId, userId }: { courseId: string, userId: number }) => resetDateExtension(courseId, userId), - onSettled: () => { - queryClient.invalidateQueries({ queryKey: dateExtensionsQueryKeys.all }); + onSuccess: ({ courseId }) => { + queryClient.invalidateQueries({ queryKey: dateExtensionsQueryKeys.byCourse(courseId) }); }, }); }; From bf6b5e946ece11a0dd9dcf9af523ddec6b470e03 Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Thu, 5 Feb 2026 14:16:54 -0500 Subject: [PATCH 5/9] refactor: update endpoint --- src/dateExtensions/DateExtensionsPage.tsx | 14 +++++++------- src/dateExtensions/data/api.ts | 6 +++--- src/dateExtensions/data/apiHook.ts | 7 ++++--- src/dateExtensions/types.ts | 8 +++++++- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/dateExtensions/DateExtensionsPage.tsx b/src/dateExtensions/DateExtensionsPage.tsx index 5c6c033d..ecd9cfb6 100644 --- a/src/dateExtensions/DateExtensionsPage.tsx +++ b/src/dateExtensions/DateExtensionsPage.tsx @@ -8,8 +8,6 @@ import ResetExtensionsModal from './components/ResetExtensionsModal'; import { LearnerDateExtension } from './types'; import { useResetDateExtensionMutation } from './data/apiHook'; -// 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'; - const DateExtensionsPage = () => { const intl = useIntl(); const { courseId } = useParams<{ courseId: string }>(); @@ -33,9 +31,8 @@ const DateExtensionsPage = () => { setErrorMessage(error.message); }; - const handleSuccessOnReset = (response: any) => { - const { message } = response; - setSuccessMessage(message); + const handleSuccessOnReset = (response: string) => { + setSuccessMessage(response); handleCloseModal(); }; @@ -43,7 +40,10 @@ const DateExtensionsPage = () => { if (selectedUser && courseId) { resetMutation({ courseId, - userId: selectedUser.id + params: { + student: selectedUser.username, + url: selectedUser.unitLocation, + } }, { onError: handleErrorOnReset, onSuccess: handleSuccessOnReset @@ -67,7 +67,7 @@ const DateExtensionsPage = () => { onClose={handleCloseModal} onConfirmReset={handleConfirmReset} /> - {}} className="text-break"> + setSuccessMessage('')} className="text-break"> {successMessage} setErrorMessage('')}>{intl.formatMessage(messages.close)}}> diff --git a/src/dateExtensions/data/api.ts b/src/dateExtensions/data/api.ts index 4be982b8..d57e1064 100644 --- a/src/dateExtensions/data/api.ts +++ b/src/dateExtensions/data/api.ts @@ -1,6 +1,6 @@ import { camelCaseObject, getAuthenticatedHttpClient } from '@openedx/frontend-base'; import { getApiBaseUrl } from '../../data/api'; -import { DateExtensionsResponse } from '../types'; +import { DateExtensionsResponse, ResetDueDateParams } from '../types'; export interface PaginationQueryKeys { page: number, @@ -17,7 +17,7 @@ export const getDateExtensions = async ( return camelCaseObject(data); }; -export const resetDateExtension = async (courseId, userId) => { - const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/api/instructor/v1/courses/${courseId}/date-extensions/${userId}/reset`); +export const resetDateExtension = async (courseId: string, params: ResetDueDateParams) => { + const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/courses/${courseId}/instructor/api/reset_due_date`, params); return camelCaseObject(data); }; diff --git a/src/dateExtensions/data/apiHook.ts b/src/dateExtensions/data/apiHook.ts index 6cbfefa9..90096d16 100644 --- a/src/dateExtensions/data/apiHook.ts +++ b/src/dateExtensions/data/apiHook.ts @@ -1,6 +1,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { getDateExtensions, PaginationQueryKeys, resetDateExtension } from './api'; import { dateExtensionsQueryKeys } from './queryKeys'; +import { ResetDueDateParams } from '../types'; export const useDateExtensions = (courseId: string, pagination: PaginationQueryKeys) => ( useQuery({ @@ -12,10 +13,10 @@ export const useDateExtensions = (courseId: string, pagination: PaginationQueryK export const useResetDateExtensionMutation = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ courseId, userId }: { courseId: string, userId: number }) => - resetDateExtension(courseId, userId), + mutationFn: ({ courseId, params }: { courseId: string, params: ResetDueDateParams }) => + resetDateExtension(courseId, params), onSuccess: ({ courseId }) => { - queryClient.invalidateQueries({ queryKey: dateExtensionsQueryKeys.byCourse(courseId) }); + queryClient.invalidateQueries({ queryKey: dateExtensionsQueryKeys.byCourse(courseId), exact: false}); }, }); }; diff --git a/src/dateExtensions/types.ts b/src/dateExtensions/types.ts index 76baf053..575ed2cc 100644 --- a/src/dateExtensions/types.ts +++ b/src/dateExtensions/types.ts @@ -1,10 +1,10 @@ export interface LearnerDateExtension { - id: number, username: string, fullName: string, email: string, unitTitle: string, extendedDueDate: string, + unitLocation: string, } export interface DateExtensionsResponse { @@ -13,3 +13,9 @@ export interface DateExtensionsResponse { previous: string | null, results: LearnerDateExtension[], } + +export interface ResetDueDateParams { + student: string, + url: string, + reason?: string, +} From d5b0cbd11697f51c34901400fd76166456fa5f60 Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Thu, 5 Feb 2026 14:58:23 -0500 Subject: [PATCH 6/9] fix: format date --- .../components/DateExtensionsList.test.tsx | 14 +++++++------- .../components/DateExtensionsList.tsx | 8 +++++++- src/dateExtensions/data/apiHook.ts | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/dateExtensions/components/DateExtensionsList.test.tsx b/src/dateExtensions/components/DateExtensionsList.test.tsx index 423aba88..9d818c4e 100644 --- a/src/dateExtensions/components/DateExtensionsList.test.tsx +++ b/src/dateExtensions/components/DateExtensionsList.test.tsx @@ -11,7 +11,7 @@ const mockData = [ fullName: 'Test User', email: 'test@example.com', unitTitle: 'Test Section', - extendedDueDate: '2024-01-01' + extendedDueDate: '2025-11-07T00:00:00Z' } ]; @@ -36,11 +36,11 @@ describe('DateExtensionsList', () => { (useDateExtensions as jest.Mock).mockReturnValue({ isLoading: false, data: { count: mockData.length, results: mockData } }); renderComponent({ onResetExtensions: mockResetExtensions }); const user = userEvent.setup(); - expect(screen.getByText('test_user')).toBeInTheDocument(); - expect(screen.getByText('Test User')).toBeInTheDocument(); - expect(screen.getByText('test@example.com')).toBeInTheDocument(); - expect(screen.getByText('Test Section')).toBeInTheDocument(); - expect(screen.getByText('2024-01-01')).toBeInTheDocument(); + expect(screen.getByText(mockData[0].username)).toBeInTheDocument(); + expect(screen.getByText(mockData[0].fullName)).toBeInTheDocument(); + expect(screen.getByText(mockData[0].email)).toBeInTheDocument(); + expect(screen.getByText(mockData[0].unitTitle)).toBeInTheDocument(); + expect(screen.getByText('11/07/2025, 12:00 AM')).toBeInTheDocument(); const resetExtensions = screen.getByRole('button', { name: /reset extensions/i }); expect(resetExtensions).toBeInTheDocument(); await user.click(resetExtensions); @@ -50,7 +50,7 @@ describe('DateExtensionsList', () => { it('renders empty table when no data provided', () => { (useDateExtensions as jest.Mock).mockReturnValue({ data: { count: 0, results: [] } }); renderComponent({}); - expect(screen.queryByText('test_user')).not.toBeInTheDocument(); + expect(screen.queryByText(mockData[0].username)).not.toBeInTheDocument(); expect(screen.getByText('No results found')).toBeInTheDocument(); }); }); diff --git a/src/dateExtensions/components/DateExtensionsList.tsx b/src/dateExtensions/components/DateExtensionsList.tsx index e0a66b5d..1355bf5d 100644 --- a/src/dateExtensions/components/DateExtensionsList.tsx +++ b/src/dateExtensions/components/DateExtensionsList.tsx @@ -34,7 +34,13 @@ const DateExtensionsList = ({ { accessor: 'fullName', Header: intl.formatMessage(messages.fullname) }, { accessor: 'email', Header: intl.formatMessage(messages.email) }, { accessor: 'unitTitle', Header: intl.formatMessage(messages.gradedSubsection) }, - { accessor: 'extendedDueDate', Header: intl.formatMessage(messages.extendedDueDate) }, + { + accessor: 'extendedDueDate', + Header: intl.formatMessage(messages.extendedDueDate), + Cell: ({ value }: { value: string }) => ( + intl.formatDate(value, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', timeZone: 'UTC' }) + ) + }, ]; const additionalColumns = [{ diff --git a/src/dateExtensions/data/apiHook.ts b/src/dateExtensions/data/apiHook.ts index 90096d16..ce3d6e61 100644 --- a/src/dateExtensions/data/apiHook.ts +++ b/src/dateExtensions/data/apiHook.ts @@ -16,7 +16,7 @@ export const useResetDateExtensionMutation = () => { mutationFn: ({ courseId, params }: { courseId: string, params: ResetDueDateParams }) => resetDateExtension(courseId, params), onSuccess: ({ courseId }) => { - queryClient.invalidateQueries({ queryKey: dateExtensionsQueryKeys.byCourse(courseId), exact: false}); + queryClient.invalidateQueries({ queryKey: dateExtensionsQueryKeys.byCourse(courseId), exact: false }); }, }); }; From 0aaed3b122dca2c396927381672cfd096ad62fa8 Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Thu, 5 Feb 2026 17:32:57 -0500 Subject: [PATCH 7/9] feat: adding alerts and toasts --- .../DateExtensionsPage.test.tsx | 38 +++++++++---------- src/dateExtensions/DateExtensionsPage.tsx | 22 +++++------ src/main.scss | 2 +- src/providers/AlertProvider.tsx | 8 ++-- src/testUtils.tsx | 11 ++++++ 5 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/dateExtensions/DateExtensionsPage.test.tsx b/src/dateExtensions/DateExtensionsPage.test.tsx index 9af509f7..518819be 100644 --- a/src/dateExtensions/DateExtensionsPage.test.tsx +++ b/src/dateExtensions/DateExtensionsPage.test.tsx @@ -1,9 +1,15 @@ -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { IntlProvider } from '@openedx/frontend-base'; -import { MemoryRouter, Route, Routes } from 'react-router-dom'; import DateExtensionsPage from './DateExtensionsPage'; import { useDateExtensions, useResetDateExtensionMutation } from './data/apiHook'; +import { renderWithAlertAndIntl } from '@src/testUtils'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + courseId: 'course-v1:edX+DemoX+Demo_Course', + }), +})); jest.mock('./data/apiHook', () => ({ useDateExtensions: jest.fn(), @@ -34,28 +40,18 @@ describe('DateExtensionsPage', () => { }); }); - const RenderWithRouter = () => ( - - - - } /> - - - - ); - it('renders page title', () => { - render(); + renderWithAlertAndIntl(); expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument(); }); it('renders add extension button', () => { - render(); + renderWithAlertAndIntl(); expect(screen.getByRole('button', { name: /add individual extension/i })).toBeInTheDocument(); }); it('renders date extensions list', () => { - render(); + renderWithAlertAndIntl(); expect(screen.getByText('Ed Byun')).toBeInTheDocument(); expect(screen.getByText('Three body diagrams')).toBeInTheDocument(); }); @@ -65,18 +61,18 @@ describe('DateExtensionsPage', () => { data: { count: 0, results: [] }, isLoading: true, }); - render(); + renderWithAlertAndIntl(); expect(screen.getByRole('status')).toBeInTheDocument(); }); it('renders reset link for each row', () => { - render(); + renderWithAlertAndIntl(); const resetLinks = screen.getAllByRole('button', { name: 'Reset Extensions' }); expect(resetLinks).toHaveLength(mockDateExtensions.length); }); it('opens reset modal when reset button is clicked', async () => { - render(); + renderWithAlertAndIntl(); const user = userEvent.setup(); const resetButton = screen.getByRole('button', { name: 'Reset Extensions' }); await user.click(resetButton); @@ -87,7 +83,7 @@ describe('DateExtensionsPage', () => { }); it('calls reset mutation when confirm reset is clicked', async () => { - render(); + renderWithAlertAndIntl(); const user = userEvent.setup(); const resetButton = screen.getByRole('button', { name: 'Reset Extensions' }); await user.click(resetButton); @@ -97,7 +93,7 @@ describe('DateExtensionsPage', () => { }); it('closes reset modal when cancel is clicked', async () => { - render(); + renderWithAlertAndIntl(); const user = userEvent.setup(); const resetButton = screen.getByRole('button', { name: 'Reset Extensions' }); await user.click(resetButton); diff --git a/src/dateExtensions/DateExtensionsPage.tsx b/src/dateExtensions/DateExtensionsPage.tsx index ecd9cfb6..d27c3f03 100644 --- a/src/dateExtensions/DateExtensionsPage.tsx +++ b/src/dateExtensions/DateExtensionsPage.tsx @@ -1,12 +1,13 @@ import { useState } from 'react'; import { useParams } from 'react-router-dom'; import { useIntl } from '@openedx/frontend-base'; -import { AlertModal, Button, Toast } from '@openedx/paragon'; +import { Button } from '@openedx/paragon'; import messages from './messages'; import DateExtensionsList from './components/DateExtensionsList'; import ResetExtensionsModal from './components/ResetExtensionsModal'; import { LearnerDateExtension } from './types'; import { useResetDateExtensionMutation } from './data/apiHook'; +import { useAlert } from '@src/providers/AlertProvider'; const DateExtensionsPage = () => { const intl = useIntl(); @@ -14,10 +15,10 @@ const DateExtensionsPage = () => { const { mutate: resetMutation } = useResetDateExtensionMutation(); const [isResetModalOpen, setIsResetModalOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); - const [successMessage, setSuccessMessage] = useState(''); - const [errorMessage, setErrorMessage] = useState(''); + const { showToast, showModal, removeAlert, clearAlerts } = useAlert(); const handleResetExtensions = (user: LearnerDateExtension) => { + clearAlerts(); setIsResetModalOpen(true); setSelectedUser(user); }; @@ -28,11 +29,16 @@ const DateExtensionsPage = () => { }; const handleErrorOnReset = (error: any) => { - setErrorMessage(error.message); + showModal({ + confirmText: intl.formatMessage(messages.close), + message: error.message, + variant: 'danger', + onConfirm: (id) => removeAlert(id) + }); }; const handleSuccessOnReset = (response: string) => { - setSuccessMessage(response); + showToast(response); handleCloseModal(); }; @@ -67,12 +73,6 @@ const DateExtensionsPage = () => { onClose={handleCloseModal} onConfirmReset={handleConfirmReset} /> - setSuccessMessage('')} className="text-break"> - {successMessage} - - setErrorMessage('')}>{intl.formatMessage(messages.close)}}> - {errorMessage} - ); }; diff --git a/src/main.scss b/src/main.scss index c13ac3dc..55493494 100644 --- a/src/main.scss +++ b/src/main.scss @@ -2,5 +2,5 @@ .toast-container { left: unset; - right: 1.25rem; + right: var(--pgn-spacing-toast-container-gutter-lg); } diff --git a/src/providers/AlertProvider.tsx b/src/providers/AlertProvider.tsx index e53a8bc9..f33b93f9 100644 --- a/src/providers/AlertProvider.tsx +++ b/src/providers/AlertProvider.tsx @@ -21,7 +21,7 @@ interface ModalAlert { isOpen: boolean, confirmText?: string, cancelText?: string, - onConfirm?: () => void, + onConfirm?: (id: string) => void, onCancel?: () => void, } @@ -50,7 +50,7 @@ interface AlertContextType { variant?: 'default' | 'warning' | 'danger' | 'success', confirmText?: string, cancelText?: string, - onConfirm?: () => void, + onConfirm?: (id: string) => void, onCancel?: () => void, }) => void, showInlineAlert: (message: string, variant?: 'success' | 'danger' | 'warning' | 'info', dismissible?: boolean) => string, @@ -103,7 +103,7 @@ export const AlertProvider: FC = ({ children }) => { variant?: 'default' | 'warning' | 'danger' | 'success', confirmText?: string, cancelText?: string, - onConfirm?: () => void, + onConfirm?: (id: string) => void, onCancel?: () => void, }) => { const id = `modal-${Date.now()}-${Math.random()}`; @@ -136,7 +136,7 @@ export const AlertProvider: FC = ({ children }) => { setModals(prev => { const modal = prev.find(m => m.id === id); if (modal?.onConfirm) { - modal.onConfirm(); + modal.onConfirm(id); } return prev.filter(m => m.id !== id); }); diff --git a/src/testUtils.tsx b/src/testUtils.tsx index 4f4aaf41..41b7ffb4 100644 --- a/src/testUtils.tsx +++ b/src/testUtils.tsx @@ -1,10 +1,21 @@ import { render } from '@testing-library/react'; import { IntlProvider } from '@openedx/frontend-base'; +import { AlertProvider } from './providers/AlertProvider'; export const renderWithIntl = (component) => { return render({ component }); }; +export const renderWithAlertAndIntl = (component) => { + return render( + + + {component} + + + ); +}; + export const createQueryMock = (data: any = undefined, isLoading = false) => ({ data, isLoading, From a83b4d5b9546f4d56dcf2d739cc52cac72f2e9a1 Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Thu, 20 Nov 2025 18:00:22 -0600 Subject: [PATCH 8/9] feat: add individual extension modal --- .../SpecifyLearnerField.tsx | 19 ++++ .../DateExtensionsPage.test.tsx | 1 + src/dateExtensions/DateExtensionsPage.tsx | 31 ++++++- .../components/AddExtensionModal.tsx | 92 +++++++++++++++++++ src/dateExtensions/data/api.ts | 12 +++ src/dateExtensions/data/apiHook.ts | 13 ++- src/dateExtensions/messages.ts | 35 +++++++ 7 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 src/components/SpecifyLearnerField/SpecifyLearnerField.tsx create mode 100644 src/dateExtensions/components/AddExtensionModal.tsx diff --git a/src/components/SpecifyLearnerField/SpecifyLearnerField.tsx b/src/components/SpecifyLearnerField/SpecifyLearnerField.tsx new file mode 100644 index 00000000..a7db6dea --- /dev/null +++ b/src/components/SpecifyLearnerField/SpecifyLearnerField.tsx @@ -0,0 +1,19 @@ +import { Button, FormControl, FormGroup, FormLabel } from '@openedx/paragon'; + +interface SpecifyLearnerFieldProps { + onChange: (value: string) => void, +} + +const SpecifyLearnerField = ({ onChange }: SpecifyLearnerFieldProps) => { + return ( + + Specify Learner: +
+ onChange(e.target.value)} /> + +
+
+ ); +}; + +export default SpecifyLearnerField; diff --git a/src/dateExtensions/DateExtensionsPage.test.tsx b/src/dateExtensions/DateExtensionsPage.test.tsx index 518819be..009a6858 100644 --- a/src/dateExtensions/DateExtensionsPage.test.tsx +++ b/src/dateExtensions/DateExtensionsPage.test.tsx @@ -14,6 +14,7 @@ jest.mock('react-router-dom', () => ({ jest.mock('./data/apiHook', () => ({ useDateExtensions: jest.fn(), useResetDateExtensionMutation: jest.fn(), + useAddDateExtensionMutation: jest.fn(() => ({ mutate: jest.fn() })), })); const mockDateExtensions = [ diff --git a/src/dateExtensions/DateExtensionsPage.tsx b/src/dateExtensions/DateExtensionsPage.tsx index d27c3f03..b3150bdc 100644 --- a/src/dateExtensions/DateExtensionsPage.tsx +++ b/src/dateExtensions/DateExtensionsPage.tsx @@ -6,16 +6,19 @@ import messages from './messages'; import DateExtensionsList from './components/DateExtensionsList'; import ResetExtensionsModal from './components/ResetExtensionsModal'; import { LearnerDateExtension } from './types'; -import { useResetDateExtensionMutation } from './data/apiHook'; +import { useAddDateExtensionMutation, useResetDateExtensionMutation } from './data/apiHook'; import { useAlert } from '@src/providers/AlertProvider'; +import AddExtensionModal from './components/AddExtensionModal'; const DateExtensionsPage = () => { const intl = useIntl(); - const { courseId } = useParams<{ courseId: string }>(); + const { courseId = '' } = useParams<{ courseId: string }>(); const { mutate: resetMutation } = useResetDateExtensionMutation(); + const { mutate: addExtensionMutation } = useAddDateExtensionMutation(); const [isResetModalOpen, setIsResetModalOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const { showToast, showModal, removeAlert, clearAlerts } = useAlert(); + const [isAddExtensionModalOpen, setIsAddExtensionModalOpen] = useState(false); const handleResetExtensions = (user: LearnerDateExtension) => { clearAlerts(); @@ -57,14 +60,36 @@ const DateExtensionsPage = () => { } }; + const handleOpenAddExtension = () => { + setIsAddExtensionModalOpen(true); + }; + + const handleAddExtension = ({ email_or_username, block_id, due_datetime, reason }) => { + addExtensionMutation({ courseId, extensionData: { + email_or_username, + block_id, + due_datetime, + reason + } }, { + onError: handleErrorOnReset, + onSuccess: handleSuccessOnReset + }); + }; + return (

{intl.formatMessage(messages.dateExtensionsTitle)}

filters

- +
+ setIsAddExtensionModalOpen(false)} + onSubmit={handleAddExtension} + /> void, + onSubmit: ({ email_or_username, block_id, due_datetime, reason }: { + email_or_username: string, + block_id: string, + due_datetime: string, + reason: string, + }) => void, +} + +const AddExtensionModal = ({ isOpen, title, onClose, onSubmit }: AddExtensionModalProps) => { + const intl = useIntl(); + + const options = [ + { label: 'is an example', value: 'example' }, + { label: 'another example', value: 'another' } + ]; + + const handleSubmit = () => { + onSubmit({ + email_or_username: 'dianasalas', + block_id: 'block-v1:DV-edtech+check+2025-05+type@sequential+block@a9500056bbb544ea82fad0d3957c6932', + due_datetime: '2025-01-21 00:00:00', + reason: 'Personal reasons' + }); + }; + + return ( + + +

{title}

+
+ +
+

{intl.formatMessage(messages.extensionInstructions)}

+ +
+
+
+ {}} /> +
+
+ {intl.formatMessage(messages.selectGradedSubsection)} + + { + options.map((option) => ( + {}}> + {option.label} + + )) + } + +
+
+
+
+

{intl.formatMessage(messages.defineExtension)}

+ + {intl.formatMessage(messages.extensionDate)}: + +
+ + +
+
+ + {intl.formatMessage(messages.reasonForExtension)}: + + +
+
+
+
+
+ + + + + + +
+ ); +}; + +export default AddExtensionModal; diff --git a/src/dateExtensions/data/api.ts b/src/dateExtensions/data/api.ts index d57e1064..6970cfc0 100644 --- a/src/dateExtensions/data/api.ts +++ b/src/dateExtensions/data/api.ts @@ -21,3 +21,15 @@ export const resetDateExtension = async (courseId: string, params: ResetDueDateP const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/courses/${courseId}/instructor/api/reset_due_date`, params); return camelCaseObject(data); }; + +interface AddDateExtensionParams { + email_or_username: string, + block_id: string, + due_datetime: string, + reason: string, +} + +export const addDateExtension = async (courseId, extensionData: AddDateExtensionParams) => { + const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/change_due_date`, extensionData); + return camelCaseObject(data); +}; diff --git a/src/dateExtensions/data/apiHook.ts b/src/dateExtensions/data/apiHook.ts index ce3d6e61..4556e5bb 100644 --- a/src/dateExtensions/data/apiHook.ts +++ b/src/dateExtensions/data/apiHook.ts @@ -1,5 +1,5 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { getDateExtensions, PaginationQueryKeys, resetDateExtension } from './api'; +import { getDateExtensions, PaginationQueryKeys, resetDateExtension, addDateExtension } from './api'; import { dateExtensionsQueryKeys } from './queryKeys'; import { ResetDueDateParams } from '../types'; @@ -20,3 +20,14 @@ export const useResetDateExtensionMutation = () => { }, }); }; + +export const useAddDateExtensionMutation = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ courseId, extensionData }: { courseId: string, extensionData: any }) => + addDateExtension(courseId, extensionData), + onSuccess: ({ courseId }) => { + queryClient.invalidateQueries({ queryKey: dateExtensionsQueryKeys.byCourse(courseId) }); + }, + }); +}; diff --git a/src/dateExtensions/messages.ts b/src/dateExtensions/messages.ts index 0b3db230..a2ba58d4 100644 --- a/src/dateExtensions/messages.ts +++ b/src/dateExtensions/messages.ts @@ -71,6 +71,41 @@ const messages = defineMessages({ defaultMessage: 'Close', description: 'Label for the close button in the reset modal', }, + addIndividualDueDateExtension: { + id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.title', + defaultMessage: 'Add Individual Due Date Extension', + description: 'Title for the add individual due date extension modal', + }, + addExtension: { + id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.addExtension', + defaultMessage: 'Add Extension', + description: 'Label for the add extension button', + }, + extensionInstructions: { + id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.extensionInstructions', + defaultMessage: 'To grant an extension, select a student, graded subsection, and define the extension due date and time.', + description: 'Instructions for adding an individual due date extension', + }, + defineExtension: { + id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.defineExtension', + defaultMessage: 'Define Extension', + description: 'Label for the define extension section', + }, + extensionDate: { + id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.extensionDate', + defaultMessage: 'Extension Date', + description: 'Label for the extension date field', + }, + reasonForExtension: { + id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.reasonForExtension', + defaultMessage: 'Reason for Extension', + description: 'Label for the reason for extension field', + }, + selectGradedSubsection: { + id: 'instruct.dateExtensions.page.addIndividualDueDateExtensionModal.selectGradedSubsection', + defaultMessage: 'Select Graded Subsection', + description: 'Label for the select graded subsection field', + }, }); export default messages; From 417ca1948c6aed0378a54bc1f90252490fd40d10 Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Fri, 28 Nov 2025 12:21:02 -0500 Subject: [PATCH 9/9] chore: abstract graded select --- .../components/AddExtensionModal.tsx | 105 ++++++++++-------- .../components/SelectGradedSubsection.tsx | 41 +++++++ src/dateExtensions/data/api.ts | 7 ++ src/dateExtensions/data/apiHook.ts | 11 +- src/dateExtensions/data/queryKeys.ts | 5 + 5 files changed, 120 insertions(+), 49 deletions(-) create mode 100644 src/dateExtensions/components/SelectGradedSubsection.tsx diff --git a/src/dateExtensions/components/AddExtensionModal.tsx b/src/dateExtensions/components/AddExtensionModal.tsx index 60afe62e..b22644ae 100644 --- a/src/dateExtensions/components/AddExtensionModal.tsx +++ b/src/dateExtensions/components/AddExtensionModal.tsx @@ -1,7 +1,9 @@ -import { ActionRow, Button, FormAutosuggest, FormAutosuggestOption, FormControl, FormGroup, FormLabel, ModalDialog } from '@openedx/paragon'; +import { useState } from 'react'; +import { ActionRow, Button, Form, FormControl, FormGroup, FormLabel, ModalDialog } from '@openedx/paragon'; import { useIntl } from '@openedx/frontend-base'; import SpecifyLearnerField from '../../components/SpecifyLearnerField/SpecifyLearnerField'; import messages from '../messages'; +import SelectGradedSubsection from './SelectGradedSubsection'; interface AddExtensionModalProps { isOpen: boolean, @@ -17,74 +19,83 @@ interface AddExtensionModalProps { const AddExtensionModal = ({ isOpen, title, onClose, onSubmit }: AddExtensionModalProps) => { const intl = useIntl(); + const [formData, setFormData] = useState({ + email_or_username: '', + block_id: '', + due_date: '', + due_time: '', + reason: '', + }); - const options = [ - { label: 'is an example', value: 'example' }, - { label: 'another example', value: 'another' } - ]; - - const handleSubmit = () => { + const handleSubmit = (event) => { + event.preventDefault(); + const { email_or_username, block_id, due_date, due_time, reason } = formData; onSubmit({ - email_or_username: 'dianasalas', - block_id: 'block-v1:DV-edtech+check+2025-05+type@sequential+block@a9500056bbb544ea82fad0d3957c6932', - due_datetime: '2025-01-21 00:00:00', - reason: 'Personal reasons' + email_or_username, + block_id, + due_datetime: `${due_date} ${due_time}`, + reason }); }; + const onChange = (event) => { + const { name, value } = event.target; + setFormData((prevData) => ({ + ...prevData, + [name]: value, + })); + }; + return ( - -

{title}

-
- -
-

{intl.formatMessage(messages.extensionInstructions)}

- +
+ +

{title}

+
+ +
+

{intl.formatMessage(messages.extensionInstructions)}

{}} />
- {intl.formatMessage(messages.selectGradedSubsection)} - - { - options.map((option) => ( - {}}> - {option.label} - - )) - } - +

{intl.formatMessage(messages.defineExtension)}

- - {intl.formatMessage(messages.extensionDate)}: - -
- - -
-
+ + + {intl.formatMessage(messages.extensionDate)}: + +
+ + +
+
+ {intl.formatMessage(messages.reasonForExtension)}: - -
+ +
- -
-
- - - - - - +
+
+ + + + + + +
); }; diff --git a/src/dateExtensions/components/SelectGradedSubsection.tsx b/src/dateExtensions/components/SelectGradedSubsection.tsx new file mode 100644 index 00000000..d8d4ad06 --- /dev/null +++ b/src/dateExtensions/components/SelectGradedSubsection.tsx @@ -0,0 +1,41 @@ +import { FormLabel, FormControl, FormGroup } from '@openedx/paragon'; +import { useGradedSubsections } from '../data/apiHook'; +import { useParams } from 'react-router'; + +interface SelectGradedSubsectionProps { + label?: string, + placeholder: string, + onChange: (event: React.ChangeEvent) => void, +} + +// Example API response used to test +// const options = [ +// { displayName: 'is an example', subsectionId: 'example' }, +// { displayName: 'another example', subsectionId: 'another' } +// ]; + +const SelectGradedSubsection = ({ label, placeholder, onChange }: SelectGradedSubsectionProps) => { + const { courseId = '' } = useParams<{ courseId: string }>(); + const { data = { results: [] } } = useGradedSubsections(courseId); + const selectOptions = [{ displayName: placeholder, subsectionId: '' }, ...data.results]; + const handleChange = (event: React.ChangeEvent) => { + onChange(event); + }; + + return ( + + {label && {label}} + + { + selectOptions.map((option) => ( + + )) + } + + + ); +}; + +export default SelectGradedSubsection; diff --git a/src/dateExtensions/data/api.ts b/src/dateExtensions/data/api.ts index 6970cfc0..b9db4efe 100644 --- a/src/dateExtensions/data/api.ts +++ b/src/dateExtensions/data/api.ts @@ -33,3 +33,10 @@ export const addDateExtension = async (courseId, extensionData: AddDateExtension const { data } = await getAuthenticatedHttpClient().post(`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/change_due_date`, extensionData); return camelCaseObject(data); }; + +export const getGradedSubsections = async (courseId: string) => { + const { data } = await getAuthenticatedHttpClient().get( + `${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/graded_subsections/` + ); + return camelCaseObject(data); +}; diff --git a/src/dateExtensions/data/apiHook.ts b/src/dateExtensions/data/apiHook.ts index 4556e5bb..d0da3a16 100644 --- a/src/dateExtensions/data/apiHook.ts +++ b/src/dateExtensions/data/apiHook.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { getDateExtensions, PaginationQueryKeys, resetDateExtension, addDateExtension } from './api'; -import { dateExtensionsQueryKeys } from './queryKeys'; +import { getDateExtensions, PaginationQueryKeys, resetDateExtension, addDateExtension, getGradedSubsections } from './api'; +import { dateExtensionsQueryKeys, gradedSubsectionsQueryKeys } from './queryKeys'; import { ResetDueDateParams } from '../types'; export const useDateExtensions = (courseId: string, pagination: PaginationQueryKeys) => ( @@ -31,3 +31,10 @@ export const useAddDateExtensionMutation = () => { }, }); }; + +export const useGradedSubsections = (courseId: string) => ( + useQuery({ + queryKey: gradedSubsectionsQueryKeys.byCourse(courseId), + queryFn: () => getGradedSubsections(courseId), + }) +); diff --git a/src/dateExtensions/data/queryKeys.ts b/src/dateExtensions/data/queryKeys.ts index 9bfd9845..1348c84c 100644 --- a/src/dateExtensions/data/queryKeys.ts +++ b/src/dateExtensions/data/queryKeys.ts @@ -6,3 +6,8 @@ export const dateExtensionsQueryKeys = { byCourse: (courseId: string) => [...dateExtensionsQueryKeys.all, courseId] as const, byCoursePaginated: (courseId: string, pagination: PaginationQueryKeys) => [...dateExtensionsQueryKeys.byCourse(courseId), pagination.page] as const, }; + +export const gradedSubsectionsQueryKeys = { + all: [appId, 'gradedSubsections'] as const, + byCourse: (courseId: string) => [...gradedSubsectionsQueryKeys.all, courseId] as const, +};