Skip to content

Commit 3cf56c3

Browse files
feat: search field
1 parent 1ab2d0c commit 3cf56c3

File tree

7 files changed

+90
-18
lines changed

7 files changed

+90
-18
lines changed

src/data/api.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export interface PaginationQueryKeys {
99
pageSize: number,
1010
}
1111

12+
export interface DateExtensionQueryParams extends PaginationQueryKeys {
13+
search?: string,
14+
gradedSubsection?: string,
15+
}
16+
1217
/**
1318
* Get course settings.
1419
* @param {string} courseId
@@ -22,10 +27,25 @@ export const getCourseInfo = async (courseId) => {
2227

2328
export const getDateExtensions = async (
2429
courseId: string,
25-
pagination: PaginationQueryKeys
30+
params: DateExtensionQueryParams
2631
): Promise<DateExtensionsResponse> => {
32+
const queryParams = new URLSearchParams({
33+
page: params.page.toString(),
34+
page_size: params.pageSize.toString(),
35+
});
36+
37+
// Add optional search parameter
38+
if (params.search) {
39+
queryParams.append('search', params.search);
40+
}
41+
42+
// Add optional graded subsection filter
43+
if (params.gradedSubsection) {
44+
queryParams.append('graded_subsection', params.gradedSubsection);
45+
}
46+
2747
const { data } = await getAuthenticatedHttpClient().get(
28-
`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/unit_extensions/?page=${pagination.page}&page_size=${pagination.pageSize}`
48+
`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/unit_extensions/?${queryParams.toString()}`
2949
);
3050
return camelCaseObject(data);
3151
};

src/data/apiHook.ts

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

55
const COURSE_INFO_QUERY_KEY = ['courseInfo'];
66

77
const dateExtensionsQueryKeys = {
88
all: [appId, 'dateExtensions'] as const,
99
byCourse: (courseId: string) => [...dateExtensionsQueryKeys.all, courseId] as const,
10-
byCoursePaginated: (courseId: string, pagination: PaginationQueryKeys) => [...dateExtensionsQueryKeys.byCourse(courseId), pagination.page] as const,
10+
byCoursePaginated: (courseId: string, params: DateExtensionQueryParams) => [
11+
...dateExtensionsQueryKeys.byCourse(courseId),
12+
params.page,
13+
params.search ?? '',
14+
params.gradedSubsection ?? ''
15+
] as const,
1116
};
1217

1318
const gradedSubsectionsQueryKeys = {
@@ -22,10 +27,11 @@ export const useCourseInfo = (courseId: string) => (
2227
})
2328
);
2429

25-
export const useDateExtensions = (courseId: string, pagination: PaginationQueryKeys) => (
30+
export const useDateExtensions = (courseId: string, params: DateExtensionQueryParams) => (
2631
useQuery({
27-
queryKey: dateExtensionsQueryKeys.byCoursePaginated(courseId, pagination),
28-
queryFn: () => getDateExtensions(courseId, pagination),
32+
queryKey: dateExtensionsQueryKeys.byCoursePaginated(courseId, params),
33+
queryFn: () => getDateExtensions(courseId, params),
34+
enabled: !!courseId, // Only run when courseId is available
2935
})
3036
);
3137

src/dateExtensions/DateExtensionsPage.test.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import userEvent from '@testing-library/user-event';
33
import { IntlProvider } from '@openedx/frontend-base';
44
import { MemoryRouter, Route, Routes } from 'react-router-dom';
55
import DateExtensionsPage from './DateExtensionsPage';
6-
import { useDateExtensions, useResetDateExtensionMutation } from '../data/apiHook';
6+
import { useDateExtensions, useGradedSubsections, useResetDateExtensionMutation } from '../data/apiHook';
77

88
jest.mock('../data/apiHook', () => ({
99
useDateExtensions: jest.fn(),
1010
useResetDateExtensionMutation: jest.fn(),
1111
useAddDateExtensionMutation: jest.fn(() => ({ mutate: jest.fn() })),
12+
useGradedSubsections: jest.fn(),
1213
}));
1314

1415
const mockDateExtensions = [
@@ -22,6 +23,13 @@ const mockDateExtensions = [
2223
},
2324
];
2425

26+
const mockGradedSubsections = [
27+
{
28+
subsectionId: 'subsection-1block-v1:edX+DemoX+2015+type@problem+block@618c5933b8b544e4a4cc103d3e508378',
29+
displayName: 'Three body diagrams'
30+
}
31+
];
32+
2533
const mutateMock = jest.fn();
2634

2735
describe('DateExtensionsPage', () => {
@@ -33,6 +41,10 @@ describe('DateExtensionsPage', () => {
3341
(useResetDateExtensionMutation as jest.Mock).mockReturnValue({
3442
mutate: mutateMock,
3543
});
44+
(useGradedSubsections as jest.Mock).mockReturnValue({
45+
data: { items: mockGradedSubsections },
46+
isLoading: false,
47+
});
3648
});
3749

3850
const RenderWithRouter = () => (
@@ -58,7 +70,7 @@ describe('DateExtensionsPage', () => {
5870
it('renders date extensions list', () => {
5971
render(<RenderWithRouter />);
6072
expect(screen.getByText('Ed Byun')).toBeInTheDocument();
61-
expect(screen.getByText('Three body diagrams')).toBeInTheDocument();
73+
expect(screen.getByRole('cell', { name: 'Three body diagrams' })).toBeInTheDocument();
6274
});
6375

6476
it('shows loading state on table when fetching data', () => {

src/dateExtensions/DateExtensionsPage.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { useState } from 'react';
22
import { useParams } from 'react-router-dom';
33
import { useIntl } from '@openedx/frontend-base';
4-
import { AlertModal, Button, Container, Toast } from '@openedx/paragon';
4+
import { AlertModal, Button, Container, FormControl, Icon, Toast } from '@openedx/paragon';
55
import messages from './messages';
66
import DateExtensionsList from './components/DateExtensionsList';
77
import ResetExtensionsModal from './components/ResetExtensionsModal';
88
import { LearnerDateExtension } from './types';
99
import { useAddDateExtensionMutation, useResetDateExtensionMutation } from '../data/apiHook';
1010
import AddExtensionModal from './components/AddExtensionModal';
1111
import SelectGradedSubsection from './components/SelectGradedSubsection';
12+
import { Search } from '@openedx/paragon/icons';
1213

1314
// 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';
1415

@@ -22,6 +23,8 @@ const DateExtensionsPage = () => {
2223
const [successMessage, setSuccessMessage] = useState<string>('');
2324
const [errorMessage, setErrorMessage] = useState<string>('');
2425
const [isAddExtensionModalOpen, setIsAddExtensionModalOpen] = useState(false);
26+
const [searchedLearner, setSearchedLearner] = useState<string>('');
27+
const [gradedSubsectionFilter, setGradedSubsectionFilter] = useState<string>('');
2528

2629
const handleResetExtensions = (user: LearnerDateExtension) => {
2730
setIsResetModalOpen(true);
@@ -75,15 +78,26 @@ const DateExtensionsPage = () => {
7578
<Container className="mt-4.5 mb-4 mx-4" fluid="xl">
7679
<h3>{intl.formatMessage(messages.dateExtensionsTitle)}</h3>
7780
<div className="d-flex align-items-center justify-content-between mb-3.5">
78-
<div>
81+
<div className="d-flex">
82+
<FormControl
83+
onChange={(e) => setSearchedLearner(e.target.value)}
84+
placeholder={intl.formatMessage(messages.searchLearnerPlaceholder)}
85+
trailingElement={<Icon src={Search} />}
86+
value={searchedLearner}
87+
/>
7988
<SelectGradedSubsection
8089
placeholder={intl.formatMessage(messages.allGradedSubsections)}
81-
onChange={() => {}}
90+
onChange={(e) => setGradedSubsectionFilter(e.target.value)}
91+
value={gradedSubsectionFilter}
8292
/>
8393
</div>
8494
<Button onClick={handleOpenAddExtension}>+ {intl.formatMessage(messages.addIndividualExtension)}</Button>
8595
</div>
86-
<DateExtensionsList onResetExtensions={handleResetExtensions} />
96+
<DateExtensionsList
97+
searchedLearner={searchedLearner}
98+
gradedSubsectionFilter={gradedSubsectionFilter}
99+
onResetExtensions={handleResetExtensions}
100+
/>
87101
<AddExtensionModal
88102
isOpen={isAddExtensionModalOpen}
89103
title={intl.formatMessage(messages.addIndividualDueDateExtension)}

src/dateExtensions/components/DateExtensionsList.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const DATE_EXTENSIONS_PAGE_SIZE = 25;
1616

1717
export interface DateExtensionListProps {
1818
onResetExtensions?: (user: LearnerDateExtension) => void,
19+
searchedLearner?: string,
20+
gradedSubsectionFilter?: string,
1921
}
2022

2123
interface DataTableFetchDataProps {
@@ -24,13 +26,17 @@ interface DataTableFetchDataProps {
2426

2527
const DateExtensionsList = ({
2628
onResetExtensions = () => {},
29+
searchedLearner = '',
30+
gradedSubsectionFilter = '',
2731
}: DateExtensionListProps) => {
2832
const intl = useIntl();
2933
const { courseId } = useParams();
3034
const [page, setPage] = useState(0);
3135
const { data = { count: 0, results: [] }, isLoading } = useDateExtensions(courseId ?? '', {
3236
page,
33-
pageSize: DATE_EXTENSIONS_PAGE_SIZE
37+
pageSize: DATE_EXTENSIONS_PAGE_SIZE,
38+
search: searchedLearner,
39+
gradedSubsection: gradedSubsectionFilter
3440
});
3541

3642
const pageCount = Math.ceil(data.count / DATE_EXTENSIONS_PAGE_SIZE);

src/dateExtensions/components/SelectGradedSubsection.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useParams } from 'react-router';
55
interface SelectGradedSubsectionProps {
66
label?: string,
77
placeholder: string,
8+
value?: string,
89
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void,
910
}
1011

@@ -14,18 +15,26 @@ interface SelectGradedSubsectionProps {
1415
// { displayName: 'another example', subsectionId: 'another' }
1516
// ];
1617

17-
const SelectGradedSubsection = ({ label, placeholder, onChange }: SelectGradedSubsectionProps) => {
18+
const SelectGradedSubsection = ({ label, placeholder, value, onChange }: SelectGradedSubsectionProps) => {
1819
const { courseId = '' } = useParams<{ courseId: string }>();
19-
const { data = { results: [] } } = useGradedSubsections(courseId);
20-
const selectOptions = [{ displayName: placeholder, subsectionId: '' }, ...data.results];
20+
const { data = { items: [] } } = useGradedSubsections(courseId);
21+
const selectOptions = [{ displayName: placeholder, subsectionId: '' }, ...data.items];
22+
2123
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
2224
onChange(event);
2325
};
2426

2527
return (
2628
<FormGroup size="sm">
2729
{label && <FormLabel>{label}</FormLabel>}
28-
<FormControl placeholder={placeholder} name="block_id" as="select" onChange={handleChange} size="md">
30+
<FormControl
31+
as="select"
32+
name="block_id"
33+
placeholder={placeholder}
34+
size="md"
35+
value={value}
36+
onChange={handleChange}
37+
>
2938
{
3039
selectOptions.map((option) => (
3140
<option key={option.subsectionId} value={option.subsectionId}>

src/dateExtensions/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ const messages = defineMessages({
106106
defaultMessage: 'All Graded Subsections',
107107
description: 'Label for the all graded subsections option in filters',
108108
},
109+
searchLearnerPlaceholder: {
110+
id: 'instruct.dateExtensions.page.filters.searchLearnerPlaceholder',
111+
defaultMessage: 'Search for a Learner',
112+
description: 'Placeholder text for the search learner input field',
113+
}
109114
});
110115

111116
export default messages;

0 commit comments

Comments
 (0)