Skip to content

Commit

Permalink
feat: add Classes Table component
Browse files Browse the repository at this point in the history
  • Loading branch information
sergivalero20 committed Feb 20, 2024
1 parent fd6acf8 commit bde4b17
Show file tree
Hide file tree
Showing 13 changed files with 291 additions and 2 deletions.
53 changes: 53 additions & 0 deletions src/features/Classes/ClassesPage/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import Container from '@edx/paragon/dist/Container';
import { Pagination } from '@edx/paragon';
import ClassesTable from 'features/Classes/ClassesTable';

import { updateCurrentPage } from 'features/Classes/data/slice';
import { fetchClassesData } from 'features/Classes/data/thunks';
import { initialPage } from 'features/constants';

const ClassesPage = () => {
const selectedInstitution = useSelector((state) => state.main.selectedInstitution);
const stateClasses = useSelector((state) => state.classes);
const dispatch = useDispatch();
const [currentPage, setCurrentPage] = useState(initialPage);

useEffect(() => {
if (Object.keys(selectedInstitution).length > 0) {
dispatch(fetchClassesData(selectedInstitution.id, currentPage));
}
}, [currentPage, selectedInstitution, dispatch]); // eslint-disable-line react-hooks/exhaustive-deps

const handlePagination = (targetPage) => {
setCurrentPage(targetPage);
dispatch(updateCurrentPage(targetPage));
};

return (
<Container size="xl" className="px-4">
<h2 className="title-page">Classes</h2>
<div className="page-content-container">
<ClassesTable
data={stateClasses.table.data}
count={stateClasses.table.count}
/>
{stateClasses.table.numPages > 1 && (
<Pagination
paginationLabel="paginationNavigation"
pageCount={stateClasses.table.numPages}
currentPage={currentPage}
onPageSelect={handlePagination}
variant="reduced"
className="mx-auto pagination-table"
size="small"
/>
)}
</div>
</Container>
);
};

export default ClassesPage;
55 changes: 55 additions & 0 deletions src/features/Classes/ClassesTable/_test_/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import CoursesPage from 'features/Courses/CoursesPage';
import { waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { renderWithProviders } from 'test-utils';

jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));

const mockStore = {
courses: {
table: {
data: [
{
masterCourseName: 'Demo Course 1',
numberOfClasses: 1,
missingClassesForInstructor: null,
numberOfStudents: 1,
numberOfPendingStudents: 1,
},
{
masterCourseName: 'Demo Course 2',
numberOfClasses: 1,
missingClassesForInstructor: 1,
numberOfStudents: 16,
numberOfPendingStudents: 0,
},
],
count: 2,
num_pages: 1,
current_page: 1,
},
},
};

describe('CoursesPage', () => {
it('renders courses data and pagination', async () => {
const component = renderWithProviders(
<CoursesPage />,
{ preloadedState: mockStore },
);

waitFor(() => {
expect(component.container).toHaveTextContent('Demo Course 1');
expect(component.container).toHaveTextContent('Demo Course 2');
expect(component.container).toHaveTextContent('Ready');
expect(component.container).toHaveTextContent('Missing (1)');
expect(component.container).toHaveTextContent('Pending (1)');
expect(component.container).toHaveTextContent('Complete');
expect(component.container).toHaveTextContent('1/2');
expect(component.container).toHaveTextContent('16/16');
});
});
});
42 changes: 42 additions & 0 deletions src/features/Classes/ClassesTable/columns.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* eslint-disable react/prop-types, no-nested-ternary */
import React from 'react';
import { format } from 'date-fns';

const columns = [
{
Header: 'Course',
accessor: 'masterCourseName',
},
{
Header: 'Class',
accessor: 'className',
},
{
Header: 'Start Date',
accessor: 'startDate',
Cell: ({ row }) => (row.values.startDate ? format(row.values.startDate, 'MM/dd/yy') : ''),
},
{
Header: 'End Date',
accessor: 'endDate',
Cell: ({ row }) => (row.values.endDate ? format(row.values.endDate, 'MM/dd/yy') : ''),
},
{
Header: 'Students Enrolled',
accessor: 'numberOfStudents',
},
{
Header: 'Max',
accessor: 'maxStudents',
},
{
Header: 'Instructors',
accessor: ({ instructors }) => (
<ul style={{ listStyleType: 'none', paddingLeft: 0 }}>
{instructors.map(instructor => <li key={instructor}>{`${instructor}`}</li>)}
</ul>
),
},
];

export { columns };
43 changes: 43 additions & 0 deletions src/features/Classes/ClassesTable/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';

import { IntlProvider } from 'react-intl';
import { Row, Col } from '@edx/paragon';
import DataTable from '@edx/paragon/dist/DataTable';

import { columns } from 'features/Classes/ClassesTable/columns';

const ClassesTable = ({ data, count }) => {
const COLUMNS = useMemo(() => columns, []);

return (
<IntlProvider locale="en">
<Row className="justify-content-center my-4 my-3">
<Col xs={11} className="p-0">
<DataTable
isSortable
columns={COLUMNS}
itemCount={count}
data={data}
>
<DataTable.Table />
<DataTable.EmptyTable content="No classes found." />
<DataTable.TableFooter />
</DataTable>
</Col>
</Row>
</IntlProvider>
);
};

ClassesTable.propTypes = {
data: PropTypes.arrayOf(PropTypes.shape([])),
count: PropTypes.number,
};

ClassesTable.defaultProps = {
data: [],
count: 0,
};

export default ClassesTable;
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions src/features/Classes/data/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { reducer } from 'features/Classes/data/slice';
export { fetchClassesData } from 'features/Classes/data/thunks';
51 changes: 51 additions & 0 deletions src/features/Classes/data/slice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
import { RequestStatus } from 'features/constants';

const initialState = {
table: {
currentPage: 1,
data: [],
status: RequestStatus.LOADING,
error: null,
numPages: 0,
count: 0,
},
filters: {},
};

export const classesSlice = createSlice({
name: 'classes',
initialState,
reducers: {
updateCurrentPage: (state, { payload }) => {
state.table.currentPage = payload;
},
fetchClassesDataRequest: (state) => {
state.table.status = RequestStatus.LOADING;
},
fetchClassesDataSuccess: (state, { payload }) => {
const { results, count, numPages } = payload;
state.table.status = RequestStatus.SUCCESS;
state.table.data = results;
state.table.numPages = numPages;
state.table.count = count;
},
fetchClassesDataFailed: (state) => {
state.table.status = RequestStatus.ERROR;
},
updateFilters: (state, { payload }) => {
state.filters = payload;
},
},
});

export const {
updateCurrentPage,
fetchClassesDataRequest,
fetchClassesDataSuccess,
fetchClassesDataFailed,
updateFilters,
} = classesSlice.actions;

export const { reducer } = classesSlice;
26 changes: 26 additions & 0 deletions src/features/Classes/data/thunks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { logError } from '@edx/frontend-platform/logging';
import { camelCaseObject } from '@edx/frontend-platform';
import {
fetchClassesDataRequest,
fetchClassesDataSuccess,
fetchClassesDataFailed,
} from 'features/Classes/data/slice';
import { getClassesByInstitution } from 'features/Common/data/api';

function fetchClassesData(id, currentPage) {
return async (dispatch) => {
dispatch(fetchClassesDataRequest);

try {
const response = camelCaseObject(await getClassesByInstitution(id, '', true, '', currentPage));
dispatch(fetchClassesDataSuccess(response.data));
} catch (error) {
dispatch(fetchClassesDataFailed());
logError(error);
}
};
}

export {
fetchClassesData,
};
4 changes: 2 additions & 2 deletions src/features/Common/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ function getLicensesByInstitution(institutionId, limit, page = '') {
);
}

function getClassesByInstitution(institutionId, courseName, limit = false, instructorsList = '') {
function getClassesByInstitution(institutionId, courseName, limit = false, instructorsList = '', page = '') {
const encodedCourseName = encodeURIComponent(courseName);

return getAuthenticatedHttpClient().get(
`${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/classes`
+ `/?limit=${limit}&institution_id=${institutionId}&course_name=${encodedCourseName}&instructors=${instructorsList}`,
+ `/?limit=${limit}&institution_id=${institutionId}&course_name=${encodedCourseName}&instructors=${instructorsList}&page=${page}`,
);
}

Expand Down
11 changes: 11 additions & 0 deletions src/features/Main/Sidebar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ export const Sidebar = () => {
<span className="nav-text">Courses</span>
</button>
</li>
<li>
<button
type="button"
className={`${activeTab === 'classes' ? 'active' : ''} sidebar-item`}
aria-current="page"
onClick={() => handleTabClick('classes')}
>
<i className="fa-regular fa-books" />
<span className="nav-text">Classes</span>
</button>
</li>
</ul>
</nav>
</header>
Expand Down
4 changes: 4 additions & 0 deletions src/features/Main/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import InstructorsPage from 'features/Instructors/InstructorsPage';
import CoursesPage from 'features/Courses/CoursesPage';
import DashboardPage from 'features/Dashboard/DashboardPage';
import LicensesPage from 'features/Licenses/LicensesPage';
import ClassesPage from 'features/Classes/ClassesPage';
import { Select } from 'react-paragon-topaz';

import { fetchInstitutionData } from 'features/Main/data/thunks';
Expand Down Expand Up @@ -93,6 +94,9 @@ const Main = () => {
<Switch>
<Route path="/licenses" exact component={LicensesPage} />
</Switch>
<Switch>
<Route path="/classes" exact component={ClassesPage} />
</Switch>
<Footer />
</Container>
</main>
Expand Down
2 changes: 2 additions & 0 deletions src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { reducer as coursesReducer } from 'features/Courses/data';
import { reducer as studentsReducer } from 'features/Students/data';
import { reducer as dashboardReducer } from 'features/Dashboard/data';
import { reducer as licensesReducer } from 'features/Licenses/data';
import { reducer as classesReducer } from 'features/Classes/data';

export function initializeStore(preloadedState = undefined) {
return configureStore({
Expand All @@ -15,6 +16,7 @@ export function initializeStore(preloadedState = undefined) {
students: studentsReducer,
dashboard: dashboardReducer,
licenses: licensesReducer,
classes: classesReducer,
},
preloadedState,
});
Expand Down

0 comments on commit bde4b17

Please sign in to comment.