diff --git a/src/features/Common/data/_test_/api.test.js b/src/features/Common/data/_test_/api.test.js new file mode 100644 index 00000000..807ae7e1 --- /dev/null +++ b/src/features/Common/data/_test_/api.test.js @@ -0,0 +1,35 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { getCoursesByInstitution } from 'features/Common/data/api'; + +jest.mock('@edx/frontend-platform/auth', () => ({ + getAuthenticatedHttpClient: jest.fn(), +})); + +jest.mock('@edx/frontend-platform', () => ({ + getConfig: jest.fn(() => ({ + LMS_BASE_URL: 'http://localhost:18000', + COURSE_OPERATIONS_API_V2_BASE_URL: 'http://localhost:18000/pearson_course_operation/api/v2', + })), +})); + +describe('getCoursesByInstitution', () => { + test('should call getAuthenticatedHttpClient with the correct parameters', () => { + const httpClientMock = { + get: jest.fn(), + }; + + const institutionId = 1; + + getAuthenticatedHttpClient.mockReturnValue(httpClientMock); + + getCoursesByInstitution(institutionId); + + expect(getAuthenticatedHttpClient).toHaveBeenCalledTimes(1); + expect(getAuthenticatedHttpClient).toHaveBeenCalledWith(); + + expect(httpClientMock.get).toHaveBeenCalledTimes(1); + expect(httpClientMock.get).toHaveBeenCalledWith( + 'http://localhost:18000/pearson_course_operation/api/v2/courses/?institution_id=1', + ); + }); +}); diff --git a/src/features/Common/data/api.js b/src/features/Common/data/api.js new file mode 100644 index 00000000..a85d79a8 --- /dev/null +++ b/src/features/Common/data/api.js @@ -0,0 +1,12 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { getConfig } from '@edx/frontend-platform'; + +function getCoursesByInstitution(institutionId) { + return getAuthenticatedHttpClient().get( + `${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/courses/?institution_id=${institutionId}`, + ); +} + +export { + getCoursesByInstitution, +}; diff --git a/src/features/Instructors/AddInstructors/index.jsx b/src/features/Instructors/AddInstructors/index.jsx index 208172d6..490f7751 100644 --- a/src/features/Instructors/AddInstructors/index.jsx +++ b/src/features/Instructors/AddInstructors/index.jsx @@ -1,7 +1,8 @@ import React, { useState, useReducer, useEffect } from 'react'; import { - Button, FormGroup, ModalDialog, useToggle, Form, + FormGroup, ModalDialog, useToggle, Form, } from '@edx/paragon'; +import { Button } from 'react-paragon-topaz'; import { getCCXList, handleInstructorsEnrollment } from 'features/Instructors/data/api'; import reducer from 'features/Instructors/AddInstructors/reducer'; import { logError } from '@edx/frontend-platform/logging'; @@ -62,7 +63,7 @@ const AddInstructors = () => { return ( <> - { , ); - const button = getByText('Filters'); - await act(async () => { - fireEvent.click(button); - }); - const nameInput = getByPlaceholderText('Enter Instructor Name'); const emailInput = getByPlaceholderText('Enter Instructor Email'); - const classNameInput = getByPlaceholderText('Enter Class Name'); - const buttonApplyFilters = getByText('Apply Filters'); + const buttonApplyFilters = getByText('Apply'); expect(nameInput).toBeInTheDocument(); expect(emailInput).toBeInTheDocument(); - expect(classNameInput).toBeInTheDocument(); fireEvent.change(nameInput, { target: { value: 'Name' } }); fireEvent.change(emailInput, { target: { value: 'name@example.com' } }); - fireEvent.change(classNameInput, { target: { value: 'CCX01' } }); expect(nameInput).toHaveValue('Name'); expect(emailInput).toHaveValue('name@example.com'); - expect(classNameInput).toHaveValue('CCX01'); await act(async () => { fireEvent.click(buttonApplyFilters); @@ -47,27 +38,18 @@ describe('InstructorsFilters Component', () => { , ); - const button = getByText('Filters'); - await act(async () => { - fireEvent.click(button); - }); - const nameInput = getByPlaceholderText('Enter Instructor Name'); const emailInput = getByPlaceholderText('Enter Instructor Email'); - const classNameInput = getByPlaceholderText('Enter Class Name'); - const buttonClearFilters = getByText('Clear'); + const buttonClearFilters = getByText('Reset'); expect(nameInput).toBeInTheDocument(); expect(emailInput).toBeInTheDocument(); - expect(classNameInput).toBeInTheDocument(); fireEvent.change(nameInput, { target: { value: 'Name' } }); fireEvent.change(emailInput, { target: { value: 'name@example.com' } }); - fireEvent.change(classNameInput, { target: { value: 'CCX01' } }); expect(nameInput).toHaveValue('Name'); expect(emailInput).toHaveValue('name@example.com'); - expect(classNameInput).toHaveValue('CCX01'); await act(async () => { fireEvent.click(buttonClearFilters); @@ -75,7 +57,6 @@ describe('InstructorsFilters Component', () => { expect(nameInput).toHaveValue(''); expect(emailInput).toHaveValue(''); - expect(classNameInput).toHaveValue(''); expect(resetPagination).toHaveBeenCalledTimes(1); }); }); diff --git a/src/features/Instructors/InstructorsFilters/_test_/reducer.test.jsx b/src/features/Instructors/InstructorsFilters/_test_/reducer.test.jsx new file mode 100644 index 00000000..73ec946b --- /dev/null +++ b/src/features/Instructors/InstructorsFilters/_test_/reducer.test.jsx @@ -0,0 +1,63 @@ +import { + FETCH_COURSES_DATA_FAILURE, + FETCH_COURSES_DATA_REQUEST, + FETCH_COURSES_DATA_SUCCESS, +} from 'features/Instructors/actionTypes'; +import { RequestStatus } from 'features/constants'; +import reducer from 'features/Students/StudentsFilters/reducer'; + +describe('Student filter reducers', () => { + const initialState = { + courses: { + data: [], + status: RequestStatus.SUCCESS, + error: null, + }, + }; + + test('should handle FETCH_COURSES_DATA_REQUEST', () => { + const state = { + ...initialState, + courses: { + ...initialState.courses, + status: RequestStatus.LOADING, + }, + }; + const action = { + type: FETCH_COURSES_DATA_REQUEST, + }; + expect(reducer(state, action)).toEqual(state); + }); + + test('should handle FETCH_COURSES_DATA_SUCCESS', () => { + const state = { + ...initialState, + courses: { + ...initialState.courses, + status: RequestStatus.SUCCESS, + data: [], + }, + }; + const action = { + type: FETCH_COURSES_DATA_SUCCESS, + payload: [], + }; + expect(reducer(state, action)).toEqual(state); + }); + + test('should handle FETCH_COURSES_DATA_FAILURE', () => { + const state = { + ...initialState, + courses: { + ...initialState.courses, + status: RequestStatus.ERROR, + error: '', + }, + }; + const action = { + type: FETCH_COURSES_DATA_FAILURE, + payload: '', + }; + expect(reducer(state, action)).toEqual(state); + }); +}); diff --git a/src/features/Instructors/InstructorsFilters/index.jsx b/src/features/Instructors/InstructorsFilters/index.jsx index 4284f1c5..b4d63da2 100644 --- a/src/features/Instructors/InstructorsFilters/index.jsx +++ b/src/features/Instructors/InstructorsFilters/index.jsx @@ -1,73 +1,142 @@ -import React, { useState } from 'react'; +import React, { + useState, useReducer, useContext, useEffect, +} from 'react'; import { - DropdownButton, Form, Col, Button, + Form, Col, } from '@edx/paragon'; +import { camelCaseObject } from '@edx/frontend-platform'; +import { Select, Button } from 'react-paragon-topaz'; +import { logError } from '@edx/frontend-platform/logging'; +import { InstitutionContext } from 'features/Main/institutionContext'; +import reducer from 'features/Instructors/InstructorsFilters/reducer'; +import { getCoursesByInstitution } from 'features/Common/data/api'; +import { + FETCH_COURSES_DATA_REQUEST, + FETCH_COURSES_DATA_SUCCESS, + FETCH_COURSES_DATA_FAILURE, +} from 'features/Instructors/actionTypes'; +import { RequestStatus } from 'features/constants'; import PropTypes from 'prop-types'; -const initialFilterFormValues = { - instructorName: '', - instructorEmail: '', - ccxId: '', +const initialState = { + courses: { + data: [], + status: RequestStatus.SUCCESS, + error: null, + }, }; const InstructorsFilters = ({ fetchData, resetPagination }) => { - const [filters, setFilters] = useState(initialFilterFormValues); + const stateInstitution = useContext(InstitutionContext); + const [state, dispatch] = useReducer(reducer, initialState); + const [courseOptions, setCourseOptions] = useState([]); + const [instructorName, setInstructorName] = useState(''); + const [instructorEmail, setInstructorEmail] = useState(''); + const [courseSelected, setCourseSelected] = useState(null); + // check this after implementation of selector institution + let id = 0; + if (stateInstitution.length > 0) { + id = stateInstitution[0].id; + } - const handleInputChange = (e) => { - setFilters({ - ...filters, - [e.target.name]: e.target.value.trim(), - }); - }; + const fetchCoursesData = async () => { + dispatch({ type: FETCH_COURSES_DATA_REQUEST }); - const handleApplyFilters = async () => { - fetchData(filters); + try { + const response = camelCaseObject(await getCoursesByInstitution(id)); + dispatch({ type: FETCH_COURSES_DATA_SUCCESS, payload: response }); + } catch (error) { + dispatch({ type: FETCH_COURSES_DATA_FAILURE, payload: error }); + logError(error); + } }; const handleCleanFilters = () => { - setFilters(initialFilterFormValues); fetchData(); resetPagination(); + setInstructorName(''); + setInstructorEmail(''); + setCourseSelected(null); + }; + + const handleInstructorsFilter = async (e) => { + e.preventDefault(); + const form = e.target; + const formData = new FormData(form); + const formJson = Object.fromEntries(formData.entries()); + try { + fetchData(formJson); + } catch (error) { + logError(error); + } }; + useEffect(() => { + if (id > 0) { + fetchCoursesData(); + } // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); + + useEffect(() => { + if (state.courses.data.length > 0) { + const options = state.courses.data.map(course => ({ + ...course, + label: course.masterCourseName, + value: course.masterCourseName, + })); + setCourseOptions(options); + } + }, [state.courses]); + return ( - -
- - - - -
- - +
+
+

Search

+ + + + setInstructorName(e.target.value)} + value={instructorName} + /> + + + setInstructorEmail(e.target.value)} + value={instructorEmail} + /> + + +
+ + +