Skip to content

Commit

Permalink
feat: redux store to courses and students page
Browse files Browse the repository at this point in the history
  • Loading branch information
AuraAlba committed Jan 5, 2024
1 parent 580441c commit b613836
Show file tree
Hide file tree
Showing 40 changed files with 812 additions and 859 deletions.
6 changes: 0 additions & 6 deletions jsconfig.json

This file was deleted.

82 changes: 50 additions & 32 deletions src/features/Courses/CoursesFilters/_test_/index.test.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
/* eslint-disable react/prop-types */
import MockAdapter from 'axios-mock-adapter';
import React from 'react';
import { Provider } from 'react-redux';
import { render, fireEvent, act } from '@testing-library/react';
import CoursesFilters from 'features/Courses/CoursesFilters';
import '@testing-library/jest-dom/extend-expect';
import { initializeStore } from 'store';
import { fetchCoursesData } from 'features/Courses/data/thunks';
import { executeThunk } from 'test-utils';
import { initializeMockApp } from '@edx/frontend-platform/testing';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

let axiosMock;
let store;

jest.mock('react-select', () => function reactSelect({ options, valueR, onChange }) {
function handleChange(event) {
Expand All @@ -23,42 +33,56 @@ jest.mock('react-select', () => function reactSelect({ options, valueR, onChange
);
});

describe('InstructorsFilters Component', () => {
const mockSetFilters = jest.fn();

beforeEach(() => {
mockSetFilters.mockClear();
});

const dataCourses = [
const mockResponse = {
results: [
{
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('CoursesFilters Component', () => {
const mockSetFilters = jest.fn();

beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 1,
username: 'testuser',
administrator: true,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
mockSetFilters.mockClear();
store = initializeStore();

const coursesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/courses/?limit=true&institution_id=1`;
axiosMock.onGet(coursesApiUrl)
.reply(200, mockResponse);
});

afterEach(() => {
axiosMock.reset();
});

test('call service when apply filters', async () => {
const fetchData = jest.fn();
const resetPagination = jest.fn();
const { getByText, getByTestId } = render(
<CoursesFilters
fetchData={fetchData}
resetPagination={resetPagination}
setFilters={mockSetFilters}
dataCourses={dataCourses}
/>,
<Provider store={store}>
<CoursesFilters resetPagination={resetPagination} />
</Provider>,
);

await executeThunk(fetchCoursesData(1), store.dispatch, store.getState);

const courseSelect = getByTestId('select');
const buttonApplyFilters = getByText('Apply');

Expand All @@ -71,19 +95,14 @@ describe('InstructorsFilters Component', () => {
await act(async () => {
fireEvent.click(buttonApplyFilters);
});
expect(fetchData).toHaveBeenCalledTimes(1);
});

test('clear filters', async () => {
const fetchData = jest.fn();
const resetPagination = jest.fn();
const { getByText, getByTestId } = render(
<CoursesFilters
fetchData={fetchData}
resetPagination={resetPagination}
setFilters={mockSetFilters}
dataCourses={dataCourses}
/>,
<Provider store={store}>
<CoursesFilters resetPagination={resetPagination} />
</Provider>,
);

const courseSelect = getByTestId('select');
Expand All @@ -97,6 +116,5 @@ describe('InstructorsFilters Component', () => {
await act(async () => {
fireEvent.click(buttonClearFilters);
});
expect(resetPagination).toHaveBeenCalledTimes(1);
});
});
34 changes: 21 additions & 13 deletions src/features/Courses/CoursesFilters/index.jsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,57 @@
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { Col, Form } from '@edx/paragon';
import { Select, Button } from 'react-paragon-topaz';
import { logError } from '@edx/frontend-platform/logging';
import PropTypes from 'prop-types';

const CoursesFilters = ({
fetchData, resetPagination, dataCourses, setFilters,
}) => {
import { updateFilters, updateCurrentPage } from 'features/Courses/data/slice';
import { fetchCoursesData } from 'features/Courses/data/thunks';
import { initialPage } from 'features/constants';

const CoursesFilters = ({ resetPagination }) => {
const dispatch = useDispatch();
const stateInstitution = useSelector((state) => state.main.institution.data);
const stateCourses = useSelector((state) => state.courses.table.data);
const [courseOptions, setCourseOptions] = useState([]);
const [courseSelected, setCourseSelected] = useState(null);
let id = '';
if (stateInstitution.length === 1) {
id = stateInstitution[0].id;
}

const handleCoursesFilter = async (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const formJson = Object.fromEntries(formData.entries());
setFilters(formJson);
dispatch(updateFilters(formJson));
try {
fetchData(formJson);
dispatch(updateCurrentPage(initialPage));
dispatch(fetchCoursesData(id, initialPage, formJson));
} catch (error) {
logError(error);
}
};

const handleCleanFilters = () => {
fetchData();
dispatch(fetchCoursesData(id));
resetPagination();
setCourseSelected(null);
setFilters({});
dispatch(updateFilters({}));
};

useEffect(() => {
if (dataCourses.length > 0) {
const options = dataCourses.map(course => ({
if (stateCourses.length > 0) {
const options = stateCourses.map(course => ({
...course,
label: course.masterCourseName,
value: course.masterCourseName,
}));
setCourseOptions(options);
}
}, [dataCourses]);
}, [stateCourses]);

return (
<div className="filter-container justify-content-center row">
Expand Down Expand Up @@ -70,10 +81,7 @@ const CoursesFilters = ({
};

CoursesFilters.propTypes = {
fetchData: PropTypes.func.isRequired,
resetPagination: PropTypes.func.isRequired,
dataCourses: PropTypes.instanceOf(Array).isRequired,
setFilters: PropTypes.func.isRequired,
};

export default CoursesFilters;
71 changes: 0 additions & 71 deletions src/features/Courses/CoursesPage/_test_/reducer.test.jsx

This file was deleted.

70 changes: 17 additions & 53 deletions src/features/Courses/CoursesPage/index.jsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,52 @@
import React, {
useEffect, useState, useReducer,
} from 'react';
import { useSelector } from 'react-redux';
import { camelCaseObject } from '@edx/frontend-platform';
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { logError } from '@edx/frontend-platform/logging';
import Container from '@edx/paragon/dist/Container';
import { Pagination } from '@edx/paragon';
import CoursesTable from 'features/Courses/CoursesTable';
import CoursesFilters from 'features/Courses/CoursesFilters';
import reducer from 'features/Courses/CoursesPage/reducer';

import { getCoursesByInstitution } from 'features/Common/data/api';
import {
FETCH_COURSES_DATA_REQUEST,
FETCH_COURSES_DATA_SUCCESS,
FETCH_COURSES_DATA_FAILURE,
UPDATE_CURRENT_PAGE,
} from 'features/Courses/actionTypes';
import { RequestStatus } from 'features/constants';

const initialState = {
data: [],
status: RequestStatus.SUCCESS,
error: null,
currentPage: 1,
numPages: 0,
};
import { updateCurrentPage } from 'features/Courses/data/slice';
import { fetchCoursesData } from 'features/Courses/data/thunks';
import { initialPage } from 'features/constants';

const CoursesPage = () => {
const stateInstitution = useSelector((state) => state.main.institution.data);
const [state, dispatch] = useReducer(reducer, initialState);
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState({});
const stateCourses = useSelector((state) => state.courses);
const dispatch = useDispatch();
const [currentPage, setCurrentPage] = useState(initialPage);
// check this after implementation of selector institution
let id = '';
if (stateInstitution.length === 1) {
id = stateInstitution[0].id;
}

const fetchData = async (filtersData) => {
dispatch({ type: FETCH_COURSES_DATA_REQUEST });

try {
const response = camelCaseObject(await getCoursesByInstitution(id, true, currentPage, filtersData));
dispatch({ type: FETCH_COURSES_DATA_SUCCESS, payload: response.data });
} catch (error) {
dispatch({ type: FETCH_COURSES_DATA_FAILURE, payload: error });
logError(error);
}
};

useEffect(() => {
fetchData(filters);
}, [currentPage, id, filters]); // eslint-disable-line react-hooks/exhaustive-deps
dispatch(fetchCoursesData(id, currentPage, stateCourses.filters));
}, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps

const handlePagination = (targetPage) => {
setCurrentPage(targetPage);
dispatch({ type: UPDATE_CURRENT_PAGE, payload: targetPage });
fetchData();
dispatch(updateCurrentPage(targetPage));
};

const resetPagination = () => {
setCurrentPage(1);
setCurrentPage(initialPage);
};

return (
<Container size="xl" className="px-4">
<h2 className="title-page">Courses</h2>
<div className="page-content-container">
<CoursesFilters
dataCourses={state.data}
fetchData={fetchData}
resetPagination={resetPagination}
setFilters={setFilters}
/>
<CoursesFilters resetPagination={resetPagination} />
<CoursesTable
data={state.data}
count={state.count}
data={stateCourses.table.data}
count={stateCourses.table.count}
/>
{state.numPages > 1 && (
{stateCourses.table.numPages > 1 && (
<Pagination
paginationLabel="paginationNavigation"
pageCount={state.numPages}
pageCount={stateCourses.table.numPages}
currentPage={currentPage}
onPageSelect={handlePagination}
variant="reduced"
Expand Down
Loading

0 comments on commit b613836

Please sign in to comment.