Skip to content

Commit 4486a51

Browse files
refactor: update endpoints and improvements
1 parent 47be9b2 commit 4486a51

File tree

14 files changed

+244
-91
lines changed

14 files changed

+244
-91
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import SpecifyLearnerField from './SpecifyLearnerField';
4+
import messages from './messages';
5+
import { renderWithIntl } from '@src/testUtils';
6+
7+
describe('SpecifyLearnerField', () => {
8+
it('renders label and input', () => {
9+
renderWithIntl(<SpecifyLearnerField onChange={jest.fn()} />);
10+
expect(screen.getByText(messages.specifyLearner.defaultMessage)).toBeInTheDocument();
11+
expect(screen.getByPlaceholderText(messages.specifyLearnerPlaceholder.defaultMessage)).toBeInTheDocument();
12+
});
13+
14+
it('renders select button', () => {
15+
renderWithIntl(<SpecifyLearnerField onChange={jest.fn()} />);
16+
expect(screen.getByText(messages.select.defaultMessage)).toBeInTheDocument();
17+
});
18+
19+
it('calls onChange when input changes', async () => {
20+
const handleChange = jest.fn();
21+
renderWithIntl(<SpecifyLearnerField onChange={handleChange} />);
22+
const input = screen.getByPlaceholderText(messages.specifyLearnerPlaceholder.defaultMessage);
23+
const user = userEvent.setup();
24+
await user.type(input, 'testuser');
25+
expect(handleChange).toHaveBeenCalled();
26+
});
27+
28+
it('input has correct name attribute', () => {
29+
renderWithIntl(<SpecifyLearnerField onChange={jest.fn()} />);
30+
const input = screen.getByPlaceholderText(messages.specifyLearnerPlaceholder.defaultMessage);
31+
expect(input).toHaveAttribute('name', 'emailOrUsername');
32+
});
33+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Button, FormControl, FormGroup, FormLabel } from '@openedx/paragon';
2+
import { useIntl } from '@openedx/frontend-base';
3+
import messages from './messages';
4+
5+
interface SpecifyLearnerFieldProps {
6+
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
7+
}
8+
9+
const SpecifyLearnerField = ({ onChange }: SpecifyLearnerFieldProps) => {
10+
const intl = useIntl();
11+
12+
return (
13+
<FormGroup size="sm">
14+
<FormLabel>{intl.formatMessage(messages.specifyLearner)}</FormLabel>
15+
<div className="d-flex">
16+
<FormControl className="mr-2" name="emailOrUsername" placeholder={intl.formatMessage(messages.specifyLearnerPlaceholder)} size="md" autoResize onChange={onChange} />
17+
<Button>{intl.formatMessage(messages.select)}</Button>
18+
</div>
19+
</FormGroup>
20+
);
21+
};
22+
23+
export default SpecifyLearnerField;

src/components/SpecifyLearnerField/SpecifyLearnerField.tsx

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/components/messages.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { defineMessages } from '@openedx/frontend-base';
2+
3+
const messages = defineMessages({
4+
select: {
5+
id: 'instruct.specifyLearner.select',
6+
defaultMessage: 'Select',
7+
description: 'Label for select dropdown in specify learner field',
8+
},
9+
specifyLearner: {
10+
id: 'instruct.specifyLearner.label',
11+
defaultMessage: 'Specify Learner:',
12+
description: 'Label for specify learner field',
13+
},
14+
specifyLearnerPlaceholder: {
15+
id: 'instruct.specifyLearner.placeholder',
16+
defaultMessage: 'Learner email address or username',
17+
description: 'Placeholder text for specify learner input field',
18+
},
19+
});
20+
21+
export default messages;

src/dateExtensions/DateExtensionsPage.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { LearnerDateExtension } from './types';
99
import { useAddDateExtensionMutation, useResetDateExtensionMutation } from './data/apiHook';
1010
import { useAlert } from '@src/providers/AlertProvider';
1111
import AddExtensionModal from './components/AddExtensionModal';
12+
import { APIError } from '@src/types';
1213

1314
const DateExtensionsPage = () => {
1415
const intl = useIntl();
@@ -29,10 +30,10 @@ const DateExtensionsPage = () => {
2930
setSelectedUser(null);
3031
};
3132

32-
const handleErrorOnReset = (error: Error) => {
33+
const handleErrorOnReset = (error: APIError | Error) => {
3334
showModal({
3435
confirmText: intl.formatMessage(messages.close),
35-
message: error.message,
36+
message: 'response' in error ? error.response.data.error : error.message,
3637
variant: 'danger',
3738
onConfirm: (id) => removeAlert(id)
3839
});
@@ -69,15 +70,18 @@ const DateExtensionsPage = () => {
6970
setIsAddExtensionModalOpen(true);
7071
};
7172

72-
const handleAddExtension = ({ email_or_username, block_id, due_datetime, reason }) => {
73+
const handleAddExtension = ({ emailOrUsername, blockId, dueDatetime, reason }) => {
7374
addExtensionMutation({ courseId, extensionData: {
74-
email_or_username,
75-
block_id,
76-
due_datetime,
75+
emailOrUsername,
76+
blockId,
77+
dueDatetime,
7778
reason
7879
} }, {
7980
onError: handleErrorOnReset,
80-
onSuccess: handleSuccessOnReset
81+
onSuccess: (response) => {
82+
setIsAddExtensionModalOpen(false);
83+
showToast(response.message);
84+
}
8185
});
8286
};
8387

src/dateExtensions/components/AddExtensionModal.tsx

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,44 @@
11
import { useState } from 'react';
22
import { ActionRow, Button, Form, FormControl, FormGroup, FormLabel, ModalDialog } from '@openedx/paragon';
33
import { useIntl } from '@openedx/frontend-base';
4-
import SpecifyLearnerField from '../../components/SpecifyLearnerField/SpecifyLearnerField';
4+
import SpecifyLearnerField from '../../components/SpecifyLearnerField';
55
import messages from '../messages';
66
import SelectGradedSubsection from './SelectGradedSubsection';
77

88
interface AddExtensionModalProps {
99
isOpen: boolean,
1010
title: string,
1111
onClose: () => void,
12-
onSubmit: ({ email_or_username, block_id, due_datetime, reason }: {
13-
email_or_username: string,
14-
block_id: string,
15-
due_datetime: string,
12+
onSubmit: ({ emailOrUsername, blockId, dueDatetime, reason }: {
13+
emailOrUsername: string,
14+
blockId: string,
15+
dueDatetime: string,
1616
reason: string,
1717
}) => void,
1818
}
1919

2020
const AddExtensionModal = ({ isOpen, title, onClose, onSubmit }: AddExtensionModalProps) => {
2121
const intl = useIntl();
2222
const [formData, setFormData] = useState({
23-
email_or_username: '',
24-
block_id: '',
25-
due_date: '',
26-
due_time: '',
23+
emailOrUsername: '',
24+
blockId: '',
25+
dueDate: '',
26+
dueTime: '',
2727
reason: '',
2828
});
2929

30-
const handleSubmit = (event) => {
30+
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
3131
event.preventDefault();
32-
const { email_or_username, block_id, due_date, due_time, reason } = formData;
32+
const { emailOrUsername, blockId, dueDate, dueTime, reason } = formData;
3333
onSubmit({
34-
email_or_username,
35-
block_id,
36-
due_datetime: `${due_date} ${due_time}`,
34+
emailOrUsername,
35+
blockId,
36+
dueDatetime: new Date(`${dueDate}T${dueTime}`).toISOString(),
3737
reason
3838
});
3939
};
4040

41-
const onChange = (event) => {
41+
const onChange = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
4242
const { name, value } = event.target;
4343
setFormData((prevData) => ({
4444
...prevData,
@@ -48,17 +48,19 @@ const AddExtensionModal = ({ isOpen, title, onClose, onSubmit }: AddExtensionMod
4848

4949
return (
5050
<ModalDialog isOpen={isOpen} onClose={onClose} title={title} isOverflowVisible={false} size="xl">
51-
<Form onSubmit={handleSubmit}>
52-
<ModalDialog.Header className="p-3 pl-4">
53-
<h3>{title}</h3>
54-
</ModalDialog.Header>
55-
<ModalDialog.Body className="border-bottom border-top">
51+
<ModalDialog.Header className="p-3 pl-4 border-bottom">
52+
<ModalDialog.Title as="h3" className="m-0">
53+
{title}
54+
</ModalDialog.Title>
55+
</ModalDialog.Header>
56+
<Form onSubmit={handleSubmit} className="position-relative overflow-auto">
57+
<ModalDialog.Body>
5658
<div className="pt-3">
5759
<p>{intl.formatMessage(messages.extensionInstructions)}</p>
5860
<div className="container-fluid border-bottom mb-4.5 pb-3">
5961
<div className="row">
6062
<div className="col-sm-12 col-md-6">
61-
<SpecifyLearnerField onChange={() => {}} />
63+
<SpecifyLearnerField onChange={onChange} />
6264
</div>
6365
<div className="col-sm-12 col-md-4">
6466
<SelectGradedSubsection
@@ -76,20 +78,20 @@ const AddExtensionModal = ({ isOpen, title, onClose, onSubmit }: AddExtensionMod
7678
{intl.formatMessage(messages.extensionDate)}:
7779
</FormLabel>
7880
<div className="d-md-flex w-md-50 align-items-center">
79-
<FormControl name="due_date" type="date" size="md" />
80-
<FormControl name="due_time" type="time" size="md" className="mt-sm-3 mt-md-0" />
81+
<FormControl name="dueDate" type="date" size="md" onChange={onChange} />
82+
<FormControl name="dueTime" type="time" size="md" className="mt-sm-3 mt-md-0" onChange={onChange} />
8183
</div>
8284
</FormGroup>
8385
<FormGroup className="mt-3" size="sm">
8486
<FormLabel>
8587
{intl.formatMessage(messages.reasonForExtension)}:
8688
</FormLabel>
87-
<FormControl name="reason" placeholder={intl.formatMessage(messages.reasonForExtension)} size="md" />
89+
<FormControl name="reason" placeholder={intl.formatMessage(messages.reasonForExtension)} size="md" onChange={onChange} />
8890
</FormGroup>
8991
</div>
9092
</div>
9193
</ModalDialog.Body>
92-
<ModalDialog.Footer className="p-4">
94+
<ModalDialog.Footer className="p-4 border-top">
9395
<ActionRow>
9496
<Button variant="tertiary" onClick={onClose}>{intl.formatMessage(messages.cancel)}</Button>
9597
<Button type="submit">{intl.formatMessage(messages.addExtension)}</Button>

src/dateExtensions/components/DateExtensionsList.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,11 @@ const DateExtensionsList = ({
2222
const intl = useIntl();
2323
const { courseId } = useParams();
2424
const [page, setPage] = useState(0);
25-
const { data = { count: 0, results: [] }, isLoading } = useDateExtensions(courseId ?? '', {
25+
const { data = { count: 0, results: [], numPages: 0 }, isLoading } = useDateExtensions(courseId ?? '', {
2626
page,
2727
pageSize: DATE_EXTENSIONS_PAGE_SIZE
2828
});
2929

30-
const pageCount = Math.ceil(data.count / DATE_EXTENSIONS_PAGE_SIZE);
31-
3230
const tableColumns = [
3331
{ accessor: 'username', Header: intl.formatMessage(messages.username) },
3432
{ accessor: 'fullName', Header: intl.formatMessage(messages.fullname) },
@@ -77,7 +75,7 @@ const DateExtensionsList = ({
7775
manualFilters
7876
manualPagination
7977
pageSize={DATE_EXTENSIONS_PAGE_SIZE}
80-
pageCount={pageCount}
78+
pageCount={data.numPages}
8179
/>
8280
);
8381
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { render, screen } from '@testing-library/react';
2+
import SelectGradedSubsection from './SelectGradedSubsection';
3+
import { useGradedSubsections } from '../data/apiHook';
4+
import userEvent from '@testing-library/user-event';
5+
6+
jest.mock('react-router-dom', () => ({
7+
useParams: () => ({ courseId: 'course-v1:edX+DemoX+Demo_Course' }),
8+
}));
9+
10+
jest.mock('../data/apiHook', () => ({
11+
useGradedSubsections: jest.fn(),
12+
}));
13+
14+
describe('SelectGradedSubsection', () => {
15+
const mockOnChange = jest.fn();
16+
17+
beforeEach(() => {
18+
jest.clearAllMocks();
19+
});
20+
21+
it('renders label when provided', () => {
22+
(useGradedSubsections as jest.Mock).mockReturnValue({ data: { items: [] } });
23+
render(
24+
<SelectGradedSubsection
25+
label="Select Subsection"
26+
placeholder="Choose..."
27+
onChange={mockOnChange}
28+
/>
29+
);
30+
expect(screen.getByText('Select Subsection')).toBeInTheDocument();
31+
});
32+
33+
it('renders placeholder as first option', () => {
34+
(useGradedSubsections as jest.Mock).mockReturnValue({ data: { items: [] } });
35+
render(
36+
<SelectGradedSubsection
37+
placeholder="Choose..."
38+
onChange={mockOnChange}
39+
/>
40+
);
41+
expect(screen.getByRole('option', { name: 'Choose...' })).toBeInTheDocument();
42+
});
43+
44+
it('renders options from data', () => {
45+
const items = [
46+
{ displayName: 'Quiz 1', subsectionId: 'sub1' },
47+
{ displayName: 'Quiz 2', subsectionId: 'sub2' },
48+
];
49+
(useGradedSubsections as jest.Mock).mockReturnValue({ data: { items } });
50+
render(
51+
<SelectGradedSubsection
52+
placeholder="Choose..."
53+
onChange={mockOnChange}
54+
/>
55+
);
56+
expect(screen.getByRole('option', { name: 'Quiz 1' })).toBeInTheDocument();
57+
expect(screen.getByRole('option', { name: 'Quiz 2' })).toBeInTheDocument();
58+
});
59+
60+
it('calls onChange when option is selected', async () => {
61+
const items = [
62+
{ displayName: 'Quiz 1', subsectionId: 'sub1' },
63+
];
64+
(useGradedSubsections as jest.Mock).mockReturnValue({ data: { items } });
65+
render(
66+
<SelectGradedSubsection
67+
placeholder="Choose..."
68+
onChange={mockOnChange}
69+
/>
70+
);
71+
const user = userEvent.setup();
72+
await user.selectOptions(screen.getByRole('combobox'), 'sub1');
73+
expect(mockOnChange).toHaveBeenCalled();
74+
});
75+
76+
it('renders without label', () => {
77+
(useGradedSubsections as jest.Mock).mockReturnValue({ data: { items: [] } });
78+
render(
79+
<SelectGradedSubsection
80+
placeholder="Choose..."
81+
onChange={mockOnChange}
82+
/>
83+
);
84+
expect(screen.queryByText('Select Subsection')).not.toBeInTheDocument();
85+
});
86+
});

0 commit comments

Comments
 (0)