Skip to content

Commit e1fd38d

Browse files
feat: detailed assessments table
1 parent aa8ea43 commit e1fd38d

File tree

7 files changed

+211
-1
lines changed

7 files changed

+211
-1
lines changed

src/openResponses/OpenResponsesPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Container } from '@openedx/paragon';
22
import ORSummary from './components/ORSummary';
3+
import DetailAssessmentsList from './components/DetailAssessmentsList';
34

45
const OpenResponsesPage = () => {
56
return (
67
<Container fluid>
78
<ORSummary />
9+
<DetailAssessmentsList />
810
</Container>
911
);
1012
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { screen, fireEvent, waitFor } from '@testing-library/react';
2+
import DetailAssessmentsList from './DetailAssessmentsList';
3+
import { useDetailAssessmentsData } from '../../data/apiHook';
4+
import { renderWithIntl } from '../../testUtils';
5+
6+
jest.mock('react-router-dom', () => ({
7+
useParams: () => ({ courseId: 'course-v1:edX+Test+2024' }),
8+
}));
9+
10+
jest.mock('../../data/apiHook', () => ({
11+
useDetailAssessmentsData: jest.fn(),
12+
}));
13+
14+
const mockResults = [
15+
{
16+
id: '1',
17+
unitName: 'Unit 1',
18+
assessment: 'Assessment 1',
19+
totalResponses: 2,
20+
training: 0,
21+
peer: 1,
22+
self: 0,
23+
waiting: 0,
24+
staff: 0,
25+
finalGradeReceived: 1,
26+
url: 'http://test-url.com',
27+
},
28+
];
29+
30+
describe('DetailAssessmentsList', () => {
31+
it('renders loading state', () => {
32+
(useDetailAssessmentsData as jest.Mock).mockReturnValue({
33+
data: { count: 0, results: [] },
34+
isLoading: true,
35+
});
36+
renderWithIntl(<DetailAssessmentsList />);
37+
expect(screen.getByRole('table')).toBeInTheDocument();
38+
});
39+
40+
it('renders table with data', () => {
41+
(useDetailAssessmentsData as jest.Mock).mockReturnValue({
42+
data: { count: 1, results: mockResults },
43+
isLoading: false,
44+
});
45+
renderWithIntl(<DetailAssessmentsList />);
46+
expect(screen.getByText(mockResults[0].unitName)).toBeInTheDocument();
47+
expect(screen.getByText(mockResults[0].assessment)).toBeInTheDocument();
48+
expect(screen.getByRole('link', { name: /View and Grade Responses/i })).toBeInTheDocument();
49+
});
50+
51+
it('renders correct number of columns', () => {
52+
(useDetailAssessmentsData as jest.Mock).mockReturnValue({
53+
data: { count: 1, results: mockResults },
54+
isLoading: false,
55+
});
56+
renderWithIntl(<DetailAssessmentsList />);
57+
expect(screen.getAllByRole('columnheader')).toHaveLength(10);
58+
});
59+
60+
it('calls fetchData on page change', async () => {
61+
(useDetailAssessmentsData as jest.Mock).mockReturnValue({
62+
data: { count: 30, results: mockResults },
63+
isLoading: false,
64+
});
65+
renderWithIntl(<DetailAssessmentsList />);
66+
const nextButton = screen.getByLabelText(/next/i);
67+
fireEvent.click(nextButton);
68+
await waitFor(() => {
69+
expect(useDetailAssessmentsData).toHaveBeenCalled();
70+
});
71+
});
72+
73+
it('renders empty state when no data', () => {
74+
(useDetailAssessmentsData as jest.Mock).mockReturnValue({
75+
data: { count: 0, results: [] },
76+
isLoading: false,
77+
});
78+
renderWithIntl(<DetailAssessmentsList />);
79+
expect(screen.getByRole('table')).toBeInTheDocument();
80+
expect(screen.getByText('No results found')).toBeInTheDocument();
81+
expect(screen.queryByText('View and Grade Responses')).not.toBeInTheDocument();
82+
});
83+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useState } from 'react';
2+
import { useParams } from 'react-router-dom';
3+
import { useIntl } from '@openedx/frontend-base';
4+
import { Button, DataTable } from '@openedx/paragon';
5+
import messages from '../messages';
6+
import { useDetailAssessmentsData } from '../../data/apiHook';
7+
8+
const DETAILS_PAGE_SIZE = 25;
9+
10+
interface DataTableFetchDataProps {
11+
pageIndex: number,
12+
}
13+
14+
// Example of api response to test on UI
15+
// const mockResults = [
16+
// {
17+
// id: '1',
18+
// unitName: 'Example Unit',
19+
// assessment: 'ORA block number 1',
20+
// totalResponses: 2,
21+
// training: 0,
22+
// peer: 1,
23+
// self: 0,
24+
// waiting: 0,
25+
// staff: 0,
26+
// finalGradeReceived: 1,
27+
// url: 'http://apps.local.openedx.io:8080/instructor/course-v1:DV-edtech+check+2025-05/open_responses'
28+
// }
29+
// ];
30+
31+
const DetailAssessmentsList = () => {
32+
const intl = useIntl();
33+
const { courseId } = useParams();
34+
const [page, setPage] = useState(0);
35+
const { data = { count: 0, results: [] }, isLoading } = useDetailAssessmentsData(courseId ?? '', {
36+
page,
37+
pageSize: DETAILS_PAGE_SIZE
38+
});
39+
40+
const pageCount = Math.ceil(data.count / DETAILS_PAGE_SIZE);
41+
42+
const tableColumns = [
43+
{ accessor: 'unitName', Header: intl.formatMessage(messages.unitName) },
44+
{ accessor: 'assessment', Header: intl.formatMessage(messages.assessment) },
45+
{ accessor: 'totalResponses', Header: intl.formatMessage(messages.totalResponses) },
46+
{ accessor: 'training', Header: intl.formatMessage(messages.training) },
47+
{ accessor: 'peer', Header: intl.formatMessage(messages.peer) },
48+
{ accessor: 'self', Header: intl.formatMessage(messages.self) },
49+
{ accessor: 'waiting', Header: intl.formatMessage(messages.waiting) },
50+
{ accessor: 'staff', Header: intl.formatMessage(messages.staff) },
51+
{ accessor: 'finalGradeReceived', Header: intl.formatMessage(messages.finalGradeReceived) },
52+
{ accessor: 'staffGrader', Header: intl.formatMessage(messages.staffGrader) }
53+
];
54+
55+
const handleFetchData = (data: DataTableFetchDataProps) => {
56+
setPage(data.pageIndex);
57+
};
58+
59+
const tableData = data.results.map(item => ({
60+
...item,
61+
staffGrader: <Button variant="link" size="inline" href={item.url}>View and Grade Responses</Button>,
62+
}));
63+
64+
return (
65+
<div className="mt-4.5">
66+
<h3>{intl.formatMessage(messages.details)}</h3>
67+
<DataTable
68+
columns={tableColumns}
69+
data={tableData}
70+
fetchData={handleFetchData}
71+
initialState={{
72+
pageIndex: page,
73+
pageSize: DETAILS_PAGE_SIZE
74+
}}
75+
isLoading={isLoading}
76+
isPaginated
77+
itemCount={data.count}
78+
manualFilters
79+
manualPagination
80+
pageCount={pageCount}
81+
pageSize={DETAILS_PAGE_SIZE}
82+
/>
83+
</div>
84+
);
85+
};
86+
87+
export default DetailAssessmentsList;

src/openResponses/data/api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,12 @@ export const getOpenResponsesData = async (courseId: string) => {
66
const { data } = await getAuthenticatedHttpClient().get(url);
77
return camelCaseObject(data);
88
};
9+
10+
export const getDetailAssessmentsData = async (
11+
courseId: string,
12+
params: Record<string, string | number | boolean> = {},
13+
) => {
14+
const url = `${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/open-responses/assessments`;
15+
const { data } = await getAuthenticatedHttpClient().get(url, { params });
16+
return camelCaseObject(data);
17+
};

src/openResponses/data/apiHook.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useQuery } from '@tanstack/react-query';
2-
import { getOpenResponsesData } from './api';
2+
import { getDetailAssessmentsData, getOpenResponsesData } from './api';
33
import { openResponsesQueryKeys } from './queryKeys';
44

55
export const useOpenResponsesData = (courseId: string) => (
@@ -9,3 +9,11 @@ export const useOpenResponsesData = (courseId: string) => (
99
enabled: !!courseId,
1010
})
1111
);
12+
13+
export const useDetailAssessmentsData = (courseId: string, params: Record<string, string | number | boolean> = {}) => (
14+
useQuery({
15+
queryKey: openResponsesQueryKeys.list(courseId, params),
16+
queryFn: () => getDetailAssessmentsData(courseId, params),
17+
enabled: !!courseId,
18+
})
19+
);

src/openResponses/data/queryKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import { appId } from '../../constants';
33
export const openResponsesQueryKeys = {
44
all: [appId, 'openResponses'],
55
byCourse: (courseId: string) => [...openResponsesQueryKeys.all, courseId],
6+
list: (courseId: string, params: Record<string, string | number | boolean> = {}) => [...openResponsesQueryKeys.byCourse(courseId), 'list', ...Object.entries(params).flat()]
67
};

src/openResponses/messages.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ const messages = defineMessages({
5050
id: 'instruct.openResponses.label.finalGradeReceived',
5151
defaultMessage: 'Final Grade Received',
5252
description: 'Label for the final grade received count'
53+
},
54+
details: {
55+
id: 'instruct.openResponses.title.details',
56+
defaultMessage: 'Details',
57+
description: 'Title for the details section in open responses'
58+
},
59+
unitName: {
60+
id: 'instruct.openResponses.table.header.unitName',
61+
defaultMessage: 'Unit Name',
62+
description: 'Label for the unit name header'
63+
},
64+
assessment: {
65+
id: 'instruct.openResponses.table.header.assessment',
66+
defaultMessage: 'Assessment',
67+
description: 'Label for the assessment header'
68+
},
69+
staffGrader: {
70+
id: 'instruct.openResponses.table.header.staffGrader',
71+
defaultMessage: 'Staff Grader',
72+
description: 'Label for the staff grader header'
5373
}
5474
});
5575

0 commit comments

Comments
 (0)