From 5b61d4f8cb735832ddca22b554c843578f90f680 Mon Sep 17 00:00:00 2001 From: Sergio Valero Date: Wed, 14 Aug 2024 16:39:07 -0400 Subject: [PATCH] feat: change course/class title by unique identifier --- .../Classes/Class/ClassPage/Actions.jsx | 18 ++++--- .../Class/ClassPage/__test__/index.test.jsx | 12 ++--- .../Classes/Class/ClassPage/index.jsx | 36 +++++++++----- src/features/Classes/ClassesTable/columns.jsx | 4 +- .../EnrollStudent/__test__/index.test.jsx | 32 +++++++++---- src/features/Classes/EnrollStudent/index.jsx | 28 ++++++----- .../InstructorCard/__test__/index.test.jsx | 8 ++-- src/features/Classes/InstructorCard/index.jsx | 25 ++++------ .../Classes/data/_test_/redux.test.js | 6 ++- src/features/Classes/data/thunks.js | 12 ++--- src/features/Common/data/_test_/api.test.js | 6 +-- src/features/Common/data/api.js | 6 +-- .../Courses/AddClass/_test_/index.test.jsx | 10 ++-- .../__test__/columns.test.jsx | 21 ++++---- .../Courses/CourseDetailTable/columns.jsx | 8 ++-- .../CoursesDetailPage/__test__/index.test.jsx | 4 +- .../Courses/CoursesDetailPage/index.jsx | 15 +++--- .../Courses/CoursesPage/_test_/index.test.jsx | 8 +++- src/features/Courses/CoursesTable/columns.jsx | 2 +- .../InstructorAssignSection/ClassCard.jsx | 3 +- .../Dashboard/WeeklySchedule/index.jsx | 2 +- .../Dashboard/data/_test_/redux.test.jsx | 6 ++- .../AddInstructors/__test__/index.test.jsx | 6 +-- .../Instructors/AddInstructors/index.jsx | 2 +- .../InstructorDetailTable/columns.jsx | 2 +- .../ManageInstructors/ListInstructors.jsx | 11 ++--- .../ManageInstructors/_test_/index.test.jsx | 9 ++-- .../Instructors/ManageInstructors/index.jsx | 48 ++++++++++++++----- .../Licenses/LicenseDetailTable/columns.jsx | 2 +- src/features/Main/index.jsx | 6 +-- .../Students/StudentsTable/columns.jsx | 2 +- 31 files changed, 209 insertions(+), 151 deletions(-) diff --git a/src/features/Classes/Class/ClassPage/Actions.jsx b/src/features/Classes/Class/ClassPage/Actions.jsx index e12989ec..ac68d0d9 100644 --- a/src/features/Classes/Class/ClassPage/Actions.jsx +++ b/src/features/Classes/Class/ClassPage/Actions.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; -import { useParams, useLocation, useHistory } from 'react-router-dom'; +import { useParams, useHistory } from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; import { Button } from 'react-paragon-topaz'; @@ -17,17 +17,15 @@ import AddClass from 'features/Courses/AddClass'; import EnrollStudent from 'features/Classes/EnrollStudent'; const Actions = ({ previousPage }) => { - const location = useLocation(); const history = useHistory(); - const { courseName, className } = useParams(); + const { courseId, classId } = useParams(); + const classIdDecoded = decodeURIComponent(classId); const classes = useSelector((state) => state.classes.allClasses.data); - const queryParams = new URLSearchParams(location.search); - const queryClassId = queryParams.get('classId')?.replaceAll(' ', '+'); - const classLink = `${getConfig().LEARNING_MICROFRONTEND_URL}/course/${queryClassId}/home`; + const classLink = `${getConfig().LEARNING_MICROFRONTEND_URL}/course/${classIdDecoded}/home`; const [classInfo] = classes.filter( - (classElement) => classElement.classId === queryClassId, + (classElement) => classElement.classId === classIdDecoded, ); const [isOpenEditModal, openEditModal, closeEditModal] = useToggle(false); @@ -37,7 +35,7 @@ const Actions = ({ previousPage }) => { const addQueryParam = useInstitutionIdQueryParam(); const handleManageButton = () => { - history.push(addQueryParam(`/manage-instructors/${courseName}/${className}?classId=${queryClassId}&previous=${previousPage}`)); + history.push(addQueryParam(`/manage-instructors/${courseId}/${classId}?previous=${previousPage}`)); }; return ( @@ -51,7 +49,7 @@ const Actions = ({ previousPage }) => { -

Class details: {classNameDecoded}

+

Class details: {classInfo.className}

diff --git a/src/features/Classes/ClassesTable/columns.jsx b/src/features/Classes/ClassesTable/columns.jsx index a04e23ec..2dacc97f 100644 --- a/src/features/Classes/ClassesTable/columns.jsx +++ b/src/features/Classes/ClassesTable/columns.jsx @@ -22,7 +22,7 @@ const columns = [ accessor: 'className', Cell: ({ row }) => ( {row.values.className} @@ -125,7 +125,7 @@ const columns = [ diff --git a/src/features/Classes/EnrollStudent/__test__/index.test.jsx b/src/features/Classes/EnrollStudent/__test__/index.test.jsx index bccf6640..885a3abf 100644 --- a/src/features/Classes/EnrollStudent/__test__/index.test.jsx +++ b/src/features/Classes/EnrollStudent/__test__/index.test.jsx @@ -8,8 +8,7 @@ import EnrollStudent from 'features/Classes/EnrollStudent'; import * as api from 'features/Students/data/api'; jest.mock('react-router-dom', () => ({ - useParams: jest.fn(() => ({ courseName: 'Demo course', className: 'demo class' })), - useLocation: jest.fn().mockReturnValue({ search: '?classId=demo class' }), + useParams: jest.fn(() => ({ courseId: 'Demo course', classId: 'ccx-v1' })), })); jest.mock('features/Students/data/api', () => ({ @@ -21,6 +20,19 @@ jest.mock('@edx/frontend-platform/logging', () => ({ logError: jest.fn(), })); +const mockStore = { + classes: { + allClasses: { + data: [ + { + classId: 'ccx-v1', + className: 'demo class', + }, + ], + }, + }, +}; + describe('EnrollStudent', () => { afterEach(() => { jest.clearAllMocks(); @@ -28,8 +40,8 @@ describe('EnrollStudent', () => { test('Should render with correct elements', () => { const { getByText, getByPlaceholderText } = renderWithProviders( - {}} queryClassId="ccx1" />, - { preloadedState: {} }, + {}} />, + { preloadedState: mockStore }, ); expect(getByText('Invite student to enroll')).toBeInTheDocument(); @@ -42,8 +54,8 @@ describe('EnrollStudent', () => { const onCloseMock = jest.fn(); const { getByPlaceholderText, getByText, getByTestId } = renderWithProviders( - , - { preloadedState: {} }, + , + { preloadedState: mockStore }, ); const handleEnrollmentsMock = jest.spyOn(api, 'handleEnrollments').mockResolvedValue({ @@ -78,8 +90,8 @@ describe('EnrollStudent', () => { }); const { getByPlaceholderText, getByText } = renderWithProviders( - , - { preloadedState: {} }, + , + { preloadedState: mockStore }, ); const emailInput = getByPlaceholderText('Enter email of the student to enroll'); @@ -101,8 +113,8 @@ describe('EnrollStudent', () => { const onCloseMock = jest.fn(); const { getByPlaceholderText, getByText, getByTestId } = renderWithProviders( - , - { preloadedState: {} }, + , + { preloadedState: mockStore }, ); const handleEnrollmentsMock = jest.spyOn(api, 'handleEnrollments').mockResolvedValue({ diff --git a/src/features/Classes/EnrollStudent/index.jsx b/src/features/Classes/EnrollStudent/index.jsx index 2c60b43c..63d96540 100644 --- a/src/features/Classes/EnrollStudent/index.jsx +++ b/src/features/Classes/EnrollStudent/index.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; @@ -21,16 +21,23 @@ import { initialPage } from 'features/constants'; import 'features/Classes/EnrollStudent/index.scss'; -const EnrollStudent = ({ isOpen, onClose, queryClassId }) => { +const EnrollStudent = ({ isOpen, onClose }) => { const dispatch = useDispatch(); - const { courseName, className } = useParams(); + const { courseId, classId } = useParams(); const [showToast, setShowToast] = useState(false); const [isLoading, setLoading] = useState(false); const [toastMessage, setToastMessage] = useState(''); const institution = useSelector((state) => state.main.selectedInstitution); - const courseNameDecoded = decodeURIComponent(courseName); - const classNameDecoded = decodeURIComponent(className); + const courseIdDecoded = decodeURIComponent(courseId); + const classIdDecoded = decodeURIComponent(classId); + + const defaultClassInfo = useMemo(() => ({ + className: '', + }), []); + + const classInfo = useSelector((state) => state.classes.allClasses.data) + .find((classElement) => classElement?.classId === classIdDecoded) || defaultClassInfo; const handleEnrollStudent = async (e) => { e.preventDefault(); @@ -48,7 +55,7 @@ const EnrollStudent = ({ isOpen, onClose, queryClassId }) => { try { setLoading(true); - const response = await handleEnrollments(formData, queryClassId); + const response = await handleEnrollments(formData, classIdDecoded); const validationEmailList = response?.data?.results; const messages = await getMessages(); const validEmails = []; @@ -85,15 +92,15 @@ const EnrollStudent = ({ isOpen, onClose, queryClassId }) => { setToastMessage(textToast); const params = { - course_name: courseNameDecoded, - class_name: classNameDecoded, + course_id: courseIdDecoded, + class_id: classIdDecoded, limit: true, }; dispatch(fetchStudentsData(institution.id, initialPage, params)); // Get the classes info updated with the new number of students enrolled. - dispatch(fetchAllClassesData(institution.id, courseNameDecoded)); + dispatch(fetchAllClassesData(institution.id, courseIdDecoded)); setShowToast(true); return onClose(); @@ -125,7 +132,7 @@ const EnrollStudent = ({ isOpen, onClose, queryClassId }) => {

- Class: {classNameDecoded} + Class: {classInfo.className}

{isLoading && (
@@ -166,7 +173,6 @@ const EnrollStudent = ({ isOpen, onClose, queryClassId }) => { EnrollStudent.propTypes = { isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, - queryClassId: PropTypes.string.isRequired, }; export default EnrollStudent; diff --git a/src/features/Classes/InstructorCard/__test__/index.test.jsx b/src/features/Classes/InstructorCard/__test__/index.test.jsx index 65987da1..bfba6db7 100644 --- a/src/features/Classes/InstructorCard/__test__/index.test.jsx +++ b/src/features/Classes/InstructorCard/__test__/index.test.jsx @@ -6,10 +6,8 @@ import InstructorCard from 'features/Classes/InstructorCard'; jest.mock('react-router-dom', () => ({ useParams: jest.fn(() => ({ - courseName: 'Demo course', - className: 'demo class', + classId: 'ccx-v1', })), - useLocation: jest.fn().mockReturnValue({ search: '?classId=demo+class' }), })); jest.mock('@edx/frontend-platform/logging', () => ({ @@ -32,7 +30,9 @@ const stateMock = { allClasses: { data: [{ startDate: '2024-02-13T17:42:22Z', - classId: 'demo+class', + classId: 'ccx-v1', + className: 'demo class', + masterCourseName: 'Demo course', instructors: ['Sam Sepiol'], numberOfStudents: 2, numberOfPendingStudents: 1, diff --git a/src/features/Classes/InstructorCard/index.jsx b/src/features/Classes/InstructorCard/index.jsx index be216652..560d61e0 100644 --- a/src/features/Classes/InstructorCard/index.jsx +++ b/src/features/Classes/InstructorCard/index.jsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { useParams, useLocation } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { formatDateRange } from 'helpers'; import { initialPage, RequestStatus } from 'features/constants'; @@ -15,22 +15,17 @@ import 'features/Classes/InstructorCard/index.scss'; const INSTRUCTORS_NUMBER = 3; const InstructorCard = () => { - const location = useLocation(); const dispatch = useDispatch(); - const { courseName, className } = useParams(); + const { classId } = useParams(); const institution = useSelector((state) => state.main.selectedInstitution); const instructors = useSelector((state) => state.instructors.selectOptions.data); const classes = useSelector((state) => state.classes.allClasses); - const courseNameDecoded = decodeURIComponent(courseName); - const classNameDecoded = decodeURIComponent(className); - - const queryParams = new URLSearchParams(location.search); - const classIdQuery = queryParams.get('classId')?.replaceAll(' ', '+'); + const classIdDecoded = decodeURIComponent(classId); const isLoadingClasses = classes.status === RequestStatus.LOADING; const [classInfo] = classes.data.filter( - (classElement) => classElement.classId === classIdQuery, + (classElement) => classElement.classId === classIdDecoded, ); const totalEnrolled = (classInfo?.numberOfStudents || 0) @@ -38,16 +33,16 @@ const InstructorCard = () => { useEffect(() => { if (institution.id) { - dispatch(fetchInstructorsOptionsData(institution.id, initialPage, { limit: false, class_id: classIdQuery })); + dispatch(fetchInstructorsOptionsData(institution.id, initialPage, { limit: false, class_id: classIdDecoded })); } return () => dispatch(resetInstructorOptions()); - }, [institution.id, classIdQuery, dispatch]); + }, [institution.id, classIdDecoded, dispatch]); return (
-

- {classNameDecoded} +

+ {classInfo?.className}

{isLoadingClasses && (
@@ -60,8 +55,8 @@ const InstructorCard = () => { )} {!isLoadingClasses && ( <> -

- {courseNameDecoded} +

+ {classInfo?.masterCourseName}

diff --git a/src/features/Classes/data/_test_/redux.test.js b/src/features/Classes/data/_test_/redux.test.js index 1d89c86d..343a566a 100644 --- a/src/features/Classes/data/_test_/redux.test.js +++ b/src/features/Classes/data/_test_/redux.test.js @@ -29,11 +29,12 @@ describe('Classes redux tests', () => { }); test('successful fetch classes data', async () => { - const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?course_name=`; + const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?course_id=`; const mockResponse = { results: [ { masterCourseName: 'Demo MasterCourse 1', + masterCourseId: 'course-v1:XXX+YYY+2023', className: 'Demo Class 1', startDate: '09/21/24', endDate: null, @@ -91,10 +92,11 @@ describe('Classes redux tests', () => { }); test('successful fetch all classes data', async () => { - const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?course_name=`; + const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?course_id=`; const mockResponse = [ { masterCourseName: 'Demo MasterCourse 1', + masterCourseId: 'course-v1:XXX+YYY+2023', className: 'Demo Class 1', startDate: '09/21/24', endDate: null, diff --git a/src/features/Classes/data/thunks.js b/src/features/Classes/data/thunks.js index b53f7213..3da0cbdf 100644 --- a/src/features/Classes/data/thunks.js +++ b/src/features/Classes/data/thunks.js @@ -13,13 +13,13 @@ import { import { getClassesByInstitution } from 'features/Common/data/api'; import { initialPage } from 'features/constants'; -function fetchClassesData(id, currentPage, courseName = '', urlParamsFilters = '', limit = true) { +function fetchClassesData(id, currentPage, courseId = '', urlParamsFilters = '', limit = true) { return async (dispatch) => { dispatch(fetchClassesDataRequest()); try { const response = camelCaseObject( - await getClassesByInstitution(id, courseName, limit, currentPage, urlParamsFilters), + await getClassesByInstitution(id, courseId, limit, currentPage, urlParamsFilters), ); dispatch(fetchClassesDataSuccess(response.data)); } catch (error) { @@ -29,12 +29,12 @@ function fetchClassesData(id, currentPage, courseName = '', urlParamsFilters = ' }; } -function fetchClassesOptionsData(id, courseName) { +function fetchClassesOptionsData(id, courseId) { return async (dispatch) => { dispatch(fetchClassesDataRequest()); try { - const response = camelCaseObject(await getClassesByInstitution(id, courseName, false)); + const response = camelCaseObject(await getClassesByInstitution(id, courseId, false)); dispatch(updateClassesOptions(response.data)); } catch (error) { dispatch(fetchClassesDataFailed()); @@ -43,13 +43,13 @@ function fetchClassesOptionsData(id, courseName) { }; } -function fetchAllClassesData(id, courseName = '', urlParamsFilters = '', limit = false) { +function fetchAllClassesData(id, courseId = '', urlParamsFilters = '', limit = false) { return async (dispatch) => { dispatch(fetchAllClassesDataRequest()); try { const response = camelCaseObject( - await getClassesByInstitution(id, courseName, limit, initialPage, urlParamsFilters), + await getClassesByInstitution(id, courseId, limit, initialPage, urlParamsFilters), ); dispatch(fetchAllClassesDataSuccess(response.data)); } catch (error) { diff --git a/src/features/Common/data/_test_/api.test.js b/src/features/Common/data/_test_/api.test.js index ccb1013a..03473732 100644 --- a/src/features/Common/data/_test_/api.test.js +++ b/src/features/Common/data/_test_/api.test.js @@ -72,18 +72,18 @@ describe('Common api services', () => { }; const institutionId = 1; - const courseName = 'ccx1'; + const courseId = 'course-v1'; getAuthenticatedHttpClient.mockReturnValue(httpClientMock); - getClassesByInstitution(institutionId, courseName); + getClassesByInstitution(institutionId, courseId); expect(getAuthenticatedHttpClient).toHaveBeenCalledTimes(1); expect(getAuthenticatedHttpClient).toHaveBeenCalledWith(); expect(httpClientMock.get).toHaveBeenCalledTimes(1); expect(httpClientMock.get).toHaveBeenCalledWith( - `${COURSE_OPERATIONS_API_V2}/classes/?course_name=ccx1`, + `${COURSE_OPERATIONS_API_V2}/classes/?course_id=course-v1`, { params: { institution_id: 1, limit: false, page: '', diff --git a/src/features/Common/data/api.js b/src/features/Common/data/api.js index 816180c1..dcfabd09 100644 --- a/src/features/Common/data/api.js +++ b/src/features/Common/data/api.js @@ -19,8 +19,8 @@ function getLicensesByInstitution(institutionId, limit, page = 1, urlParamsFilte ); } -function getClassesByInstitution(institutionId, courseName, limit = false, page = '', urlParamsFilters = '') { - const encodedCourseName = encodeURIComponent(courseName); +function getClassesByInstitution(institutionId, courseId, limit = false, page = '', urlParamsFilters = '') { + const encodedCourseId = encodeURIComponent(courseId); const params = { limit, institution_id: institutionId, @@ -28,7 +28,7 @@ function getClassesByInstitution(institutionId, courseName, limit = false, page ...urlParamsFilters, }; return getAuthenticatedHttpClient().get( - `${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?course_name=${encodedCourseName}`, + `${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?course_id=${encodedCourseId}`, { params }, ); } diff --git a/src/features/Courses/AddClass/_test_/index.test.jsx b/src/features/Courses/AddClass/_test_/index.test.jsx index 5c391ee5..f5e4ce37 100644 --- a/src/features/Courses/AddClass/_test_/index.test.jsx +++ b/src/features/Courses/AddClass/_test_/index.test.jsx @@ -26,15 +26,15 @@ const mockStore = { }; const courseInfoMocked = { - masterCourseId: 'course-v1:demo+demo1+2020', + masterCourseId: 'course-v1:XXX+YYY+2023', masterCourseName: 'Demo Course 1', }; describe('Add class modal', () => { test('render add class modal', () => { const { getByText, getByPlaceholderText } = renderWithProviders( - - + + { }} courseInfo={courseInfoMocked} /> , @@ -67,8 +67,8 @@ describe('Add class modal', () => { test('cancel button in add classmodal', () => { const { getByText, getByPlaceholderText } = renderWithProviders( - - + + { }} courseInfo={courseInfoMocked} /> , diff --git a/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx b/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx index 38f72a1c..6621b5d1 100644 --- a/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx +++ b/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx @@ -67,8 +67,8 @@ describe('columns', () => { }); renderWithProviders( - - + + , @@ -78,7 +78,10 @@ describe('columns', () => { const linkElement = screen.getByText('Class example'); expect(linkElement).toBeInTheDocument(); expect(linkElement).toHaveClass('text-truncate link'); - expect(linkElement).toHaveAttribute('href', '/courses/Demo Course 1/Class example?classId=class id&previous=courses'); + expect(linkElement).toHaveAttribute( + 'href', + `/courses/${encodeURIComponent('course-v1:XXX+YYY+2023')}/class id?previous=courses`, + ); }); test('Should render the dates', () => { @@ -166,8 +169,8 @@ describe('columns', () => { }; const component = renderWithProviders( - - + + , @@ -225,8 +228,8 @@ describe('columns', () => { }; const component = renderWithProviders( - - + + , @@ -276,8 +279,8 @@ describe('columns', () => { }; const component = renderWithProviders( - - + + , diff --git a/src/features/Courses/CourseDetailTable/columns.jsx b/src/features/Courses/CourseDetailTable/columns.jsx index 834a9c16..d6cdb61d 100644 --- a/src/features/Courses/CourseDetailTable/columns.jsx +++ b/src/features/Courses/CourseDetailTable/columns.jsx @@ -34,11 +34,11 @@ const columns = [ Header: 'Class', accessor: 'className', Cell: ({ row }) => { - const { courseName } = useParams(); + const { courseId } = useParams(); return ( {row.values.className} @@ -154,7 +154,7 @@ const columns = [ isRequestComplete: true, }); - await dispatch(fetchClassesData(institution.id, initialPage, masterCourseName)); + await dispatch(fetchClassesData(institution.id, initialPage, masterCourseId)); } catch (error) { logError(error); } finally { @@ -189,7 +189,7 @@ const columns = [ diff --git a/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx b/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx index d7b4cd10..5b78aae9 100644 --- a/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx +++ b/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx @@ -84,8 +84,8 @@ const mockStore = { describe('CoursesDetailPage', () => { test('Should render the table and the course info', async () => { const component = renderWithProviders( - - + + , diff --git a/src/features/Courses/CoursesDetailPage/index.jsx b/src/features/Courses/CoursesDetailPage/index.jsx index 7d192ccd..88cd81d2 100644 --- a/src/features/Courses/CoursesDetailPage/index.jsx +++ b/src/features/Courses/CoursesDetailPage/index.jsx @@ -24,8 +24,9 @@ import 'features/Courses/CoursesDetailPage/index.scss'; const CoursesDetailPage = () => { const history = useHistory(); const dispatch = useDispatch(); - const { courseName } = useParams(); - const courseNameDecoded = decodeURIComponent(courseName); + const { courseId } = useParams(); + + const courseIdDecoded = decodeURIComponent(courseId); const addQueryParam = useInstitutionIdQueryParam(); const institutionRef = useRef(undefined); @@ -39,7 +40,7 @@ const CoursesDetailPage = () => { }), []); const courseInfo = useSelector((state) => state.courses.table.data) - .find((course) => course?.masterCourseName === courseNameDecoded) || defaultCourseInfo; + .find((course) => course?.masterCourseId === courseIdDecoded) || defaultCourseInfo; const lastCourseInfoRef = useRef(courseInfo); useEffect(() => { @@ -67,7 +68,7 @@ const CoursesDetailPage = () => { }; if (institution.id) { - dispatch(fetchClassesData(institution.id, initialPage, courseNameDecoded)); + dispatch(fetchClassesData(institution.id, initialPage, courseIdDecoded)); dispatch(fetchCoursesData(institution.id, initialPage, null)); } @@ -75,10 +76,10 @@ const CoursesDetailPage = () => { dispatch(fetchCoursesDataSuccess(initialState)); dispatch(fetchClassesDataSuccess(initialState)); }; - }, [dispatch, institution.id, courseNameDecoded]); + }, [dispatch, institution.id, courseIdDecoded]); useEffect(() => { - dispatch(fetchClassesData(institution.id, currentPage, courseNameDecoded)); + dispatch(fetchClassesData(institution.id, currentPage, courseIdDecoded)); }, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { @@ -99,7 +100,7 @@ const CoursesDetailPage = () => { -

{courseNameDecoded}

+

{nextCourseInfo.masterCourseName}

diff --git a/src/features/Courses/CoursesPage/_test_/index.test.jsx b/src/features/Courses/CoursesPage/_test_/index.test.jsx index 56f7b863..086d84e1 100644 --- a/src/features/Courses/CoursesPage/_test_/index.test.jsx +++ b/src/features/Courses/CoursesPage/_test_/index.test.jsx @@ -16,6 +16,7 @@ const mockStore = { data: [ { masterCourseName: 'Demo Course 1', + masterCourseId: 'course-v1:XXX+YYY+2023', numberOfClasses: 1, missingClassesForInstructor: null, numberOfStudents: 1, @@ -23,6 +24,7 @@ const mockStore = { }, { masterCourseName: 'Demo Course 2', + masterCourseId: 'course-v1:ZZZ+YYY+2023', numberOfClasses: 1, missingClassesForInstructor: 1, numberOfStudents: 16, @@ -36,6 +38,7 @@ const mockStore = { selectOptions: [ { masterCourseName: 'Demo Course 1', + masterCourseId: 'course-v1:XXX+YYY+2023', numberOfClasses: 1, missingClassesForInstructor: null, numberOfStudents: 1, @@ -43,6 +46,7 @@ const mockStore = { }, { masterCourseName: 'Demo Course 2', + masterCourseId: 'course-v1:ZZZ+YYY+2023', numberOfClasses: 1, missingClassesForInstructor: 1, numberOfStudents: 16, @@ -55,8 +59,8 @@ const mockStore = { describe('CoursesPage', () => { it('renders courses data and pagination', async () => { const component = renderWithProviders( - - + + , diff --git a/src/features/Courses/CoursesTable/columns.jsx b/src/features/Courses/CoursesTable/columns.jsx index 6f6da4d1..0794b15b 100644 --- a/src/features/Courses/CoursesTable/columns.jsx +++ b/src/features/Courses/CoursesTable/columns.jsx @@ -15,7 +15,7 @@ const columns = [ { Header: 'Courses', accessor: 'masterCourseName', - Cell: ({ row }) => ({row.values.masterCourseName}), + Cell: ({ row }) => ({row.values.masterCourseName}), }, { Header: 'Classes', diff --git a/src/features/Dashboard/InstructorAssignSection/ClassCard.jsx b/src/features/Dashboard/InstructorAssignSection/ClassCard.jsx index d31920fe..90d114b9 100644 --- a/src/features/Dashboard/InstructorAssignSection/ClassCard.jsx +++ b/src/features/Dashboard/InstructorAssignSection/ClassCard.jsx @@ -14,7 +14,7 @@ const ClassCard = ({ data }) => { const addQueryParam = useInstitutionIdQueryParam(); const handleManageButton = () => { - history.push(addQueryParam(`/manage-instructors/${encodeURIComponent(data?.masterCourseName)}/${encodeURIComponent(data?.className)}?classId=${data?.classId}&previous=dashboard`)); + history.push(addQueryParam(`/manage-instructors/${encodeURIComponent(data?.masterCourseId)}/${encodeURIComponent(data?.classId)}?previous=dashboard`)); }; return ( @@ -35,6 +35,7 @@ ClassCard.propTypes = { classId: PropTypes.string, className: PropTypes.string, masterCourseName: PropTypes.string, + masterCourseId: PropTypes.string, startDate: PropTypes.string, endDate: PropTypes.string, }), diff --git a/src/features/Dashboard/WeeklySchedule/index.jsx b/src/features/Dashboard/WeeklySchedule/index.jsx index 420800c9..daac6221 100644 --- a/src/features/Dashboard/WeeklySchedule/index.jsx +++ b/src/features/Dashboard/WeeklySchedule/index.jsx @@ -79,7 +79,7 @@ const WeeklySchedule = () => {
{classInfo?.className} diff --git a/src/features/Dashboard/data/_test_/redux.test.jsx b/src/features/Dashboard/data/_test_/redux.test.jsx index ff96ae3b..237d7b9c 100644 --- a/src/features/Dashboard/data/_test_/redux.test.jsx +++ b/src/features/Dashboard/data/_test_/redux.test.jsx @@ -78,12 +78,13 @@ describe('Dashboard redux tests', () => { }); test('successful fetch classesNoInstructors data', async () => { - const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?course_name=`; + const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?course_id=`; const mockResponse = [ { classId: 'ccx-v1:demo+demo1+2020+ccx1', className: 'ccx 1', masterCourseName: 'Demo Course 1', + masterCourseId: 'course-v1:XXX+YYY+2023', instructors: [], numberOfStudents: 0, numberOfPendingStudents: 0, @@ -125,12 +126,13 @@ describe('Dashboard redux tests', () => { }); test('successful fetch classes data', async () => { - const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?course_name=`; + const classesApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?course_id=`; const mockResponse = [ { classId: 'ccx-v1:demo+demo1+2020+ccx1', className: 'ccx 1', masterCourseName: 'Demo Course 1', + masterCourseId: 'course-v1:XXX+YYY+2023', instructors: [], numberOfStudents: 0, numberOfPendingStudents: 0, diff --git a/src/features/Instructors/AddInstructors/__test__/index.test.jsx b/src/features/Instructors/AddInstructors/__test__/index.test.jsx index e6bf956d..0b0bee26 100644 --- a/src/features/Instructors/AddInstructors/__test__/index.test.jsx +++ b/src/features/Instructors/AddInstructors/__test__/index.test.jsx @@ -74,7 +74,7 @@ describe('Add instructor modal', () => { expect(getByText('First name')).toBeInTheDocument(); expect(getByText('Last name')).toBeInTheDocument(); expect(getByText('Cancel')).toBeInTheDocument(); - expect(getByText('Send invite')).toBeInTheDocument(); + expect(getByText('Add instructor')).toBeInTheDocument(); }); test('Should handle input changes correctly', () => { @@ -102,7 +102,7 @@ describe('Add instructor modal', () => { { preloadedState: mockStore }, ); - const sendButton = getByText('Send invite'); + const sendButton = getByText('Add instructor'); const emailInput = getByPlaceholderText('Enter Email of the instructor'); expect(sendButton).toBeInTheDocument(); @@ -132,7 +132,7 @@ describe('Instructor modal - Request', () => { ); const emailInput = getByPlaceholderText('Enter Email of the instructor'); - const sendButton = getByText('Send invite'); + const sendButton = getByText('Add instructor'); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.click(sendButton); diff --git a/src/features/Instructors/AddInstructors/index.jsx b/src/features/Instructors/AddInstructors/index.jsx index d234cf81..847fb04b 100644 --- a/src/features/Instructors/AddInstructors/index.jsx +++ b/src/features/Instructors/AddInstructors/index.jsx @@ -164,7 +164,7 @@ const AddInstructors = ({ isOpen, onClose }) => { )}
Cancel - +
)} diff --git a/src/features/Instructors/InstructorDetailTable/columns.jsx b/src/features/Instructors/InstructorDetailTable/columns.jsx index 67b54cbf..b127287a 100644 --- a/src/features/Instructors/InstructorDetailTable/columns.jsx +++ b/src/features/Instructors/InstructorDetailTable/columns.jsx @@ -13,7 +13,7 @@ const columns = [ accessor: 'className', Cell: ({ row }) => ( {row.values.className} diff --git a/src/features/Instructors/ManageInstructors/ListInstructors.jsx b/src/features/Instructors/ManageInstructors/ListInstructors.jsx index 688be14d..3bd4a481 100644 --- a/src/features/Instructors/ManageInstructors/ListInstructors.jsx +++ b/src/features/Instructors/ManageInstructors/ListInstructors.jsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import PropTypes from 'prop-types'; import { @@ -15,14 +15,13 @@ import { assignInstructors, fetchInstructorsOptionsData } from 'features/Instruc import { RequestStatus, initialPage } from 'features/constants'; const ListInstructors = ({ instructors, isLoadingInstructors }) => { - const location = useLocation(); const dispatch = useDispatch(); const institution = useSelector((state) => state.main.selectedInstitution); const statusAssignRequest = useSelector((state) => state.instructors.assignInstructors.status); - const queryParams = new URLSearchParams(location.search); - const classId = queryParams.get('classId')?.replaceAll(' ', '+'); + const { classId } = useParams(); + const classIdDecoded = decodeURIComponent(classId); const [isOpenModal, openModal, closeModal] = useToggle(false); const [instructorName, setInstructorName] = useState(''); @@ -44,11 +43,11 @@ const ListInstructors = ({ instructors, isLoadingInstructors }) => { unique_student_identifier: instructorUsername, rolename: 'staff', action: 'revoke', - class_id: classId, + class_id: classIdDecoded, }; await dispatch(assignInstructors(instructorData)); - dispatch(fetchInstructorsOptionsData(institution.id, initialPage, { limit: false, class_id: classId })); + dispatch(fetchInstructorsOptionsData(institution.id, initialPage, { limit: false, class_id: classIdDecoded })); } catch (error) { logError(error); setShowToast(true); diff --git a/src/features/Instructors/ManageInstructors/_test_/index.test.jsx b/src/features/Instructors/ManageInstructors/_test_/index.test.jsx index 3b4b4ed3..c7311ce7 100644 --- a/src/features/Instructors/ManageInstructors/_test_/index.test.jsx +++ b/src/features/Instructors/ManageInstructors/_test_/index.test.jsx @@ -67,8 +67,8 @@ const mockStore = { describe('Manage instructors page', () => { test('render page', async () => { const { getByText, getAllByRole, getByTestId } = renderWithProviders( - - + + , @@ -93,8 +93,9 @@ describe('Manage instructors page', () => { test('Delete instructor', async () => { const { getByText, getAllByTestId } = renderWithProviders( - - + + `/courses/${encodeURIComponent('course-v1:XXX+YYY+2023')}` + , diff --git a/src/features/Instructors/ManageInstructors/index.jsx b/src/features/Instructors/ManageInstructors/index.jsx index da25e7c7..04f4754d 100644 --- a/src/features/Instructors/ManageInstructors/index.jsx +++ b/src/features/Instructors/ManageInstructors/index.jsx @@ -1,4 +1,9 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { + useEffect, + useRef, + useState, + useMemo, +} from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams, useLocation, useHistory } from 'react-router-dom'; @@ -10,6 +15,7 @@ import AssignSection from 'features/Instructors/ManageInstructors/AssignSection' import { RequestStatus, initialPage } from 'features/constants'; import { resetClassesTable, resetClasses } from 'features/Classes/data/slice'; +import { fetchAllClassesData } from 'features/Classes/data/thunks'; import { updateFilters, resetRowSelect } from 'features/Instructors/data/slice'; import { assignInstructors, fetchInstructorsOptionsData } from 'features/Instructors/data'; import { updateActiveTab } from 'features/Main/data/slice'; @@ -28,14 +34,21 @@ const ManageInstructors = () => { const rowsSelected = useSelector((state) => state.instructors.rowsSelected); const instructorsByClass = useSelector((state) => state.instructors.selectOptions); - const { courseName, className } = useParams(); + const { courseId, classId } = useParams(); const queryParams = new URLSearchParams(location.search); - const classId = queryParams.get('classId')?.replaceAll(' ', '+'); const previousPage = queryParams.get('previous') || 'courses'; const isLoadingInstructors = instructorsByClass?.status === RequestStatus.LOADING; const isButtonDisabled = rowsSelected.length === 0; - const courseNameDecoded = decodeURIComponent(courseName); - const classNameDecoded = decodeURIComponent(className); + const classIdDecoded = decodeURIComponent(classId); + const courseIdDecoded = decodeURIComponent(courseId); + + const defaultClassInfo = useMemo(() => ({ + className: '', + masterCourseName: '', + }), []); + + const classInfo = useSelector((state) => state.classes.allClasses.data) + .find((classElement) => classElement?.classId === classIdDecoded) || defaultClassInfo; const resetValues = () => { cancelButtonRef?.current?.clearSelectionFunc(); @@ -49,7 +62,7 @@ const ManageInstructors = () => { unique_student_identifier: row, rolename: 'staff', action: 'allow', - class_id: classId, + class_id: classIdDecoded, })); const enrollmentData = { @@ -59,11 +72,15 @@ const ManageInstructors = () => { }; await dispatch(assignInstructors(enrollmentData)); - dispatch(fetchInstructorsOptionsData(selectedInstitution.id, initialPage, { limit: false, class_id: classId })); + dispatch(fetchInstructorsOptionsData( + selectedInstitution.id, + initialPage, + { limit: false, class_id: classIdDecoded }, + )); if (rowsSelected.length === 1) { - setToastMessage(`${rowsSelected[0]} has been successfully assigned to Class ${decodeURIComponent(className)}`); + setToastMessage(`${rowsSelected[0]} has been successfully assigned to Class ${classInfo.className}`); } else if (rowsSelected.length > 1) { - setToastMessage(`${rowsSelected.join()} have been successfully assigned to Class ${decodeURIComponent(className)}`); + setToastMessage(`${rowsSelected.join()} have been successfully assigned to Class ${classInfo.className}`); } setShowToast(true); } catch (error) { @@ -77,14 +94,19 @@ const ManageInstructors = () => { if (selectedInstitution.id) { // Leaves a gap time space to prevent being override by ActiveTabUpdater component setTimeout(() => dispatch(updateActiveTab(previousPage)), 100); - dispatch(fetchInstructorsOptionsData(selectedInstitution.id, initialPage, { limit: false, class_id: classId })); + dispatch(fetchInstructorsOptionsData( + selectedInstitution.id, + initialPage, + { limit: false, class_id: classIdDecoded }, + )); + dispatch(fetchAllClassesData(selectedInstitution.id, courseIdDecoded)); } return () => { dispatch(resetClassesTable()); dispatch(resetClasses()); }; - }, [dispatch, selectedInstitution.id, previousPage, classId]); + }, [dispatch, selectedInstitution.id, previousPage, classIdDecoded, courseIdDecoded]); return ( <> @@ -104,8 +126,8 @@ const ManageInstructors = () => {
-

{classNameDecoded}

-

{courseNameDecoded}

+

{classInfo.className}

+

{classInfo.masterCourseName}

diff --git a/src/features/Licenses/LicenseDetailTable/columns.jsx b/src/features/Licenses/LicenseDetailTable/columns.jsx index a81aa1a9..33b8df18 100644 --- a/src/features/Licenses/LicenseDetailTable/columns.jsx +++ b/src/features/Licenses/LicenseDetailTable/columns.jsx @@ -6,7 +6,7 @@ const columns = [ Header: 'Course', accessor: 'masterCourseName', Cell: ({ row }) => ( - {row.values.masterCourseName} + {row.values.masterCourseName} ), }, { diff --git a/src/features/Main/index.jsx b/src/features/Main/index.jsx index 426ed0e7..a1721c1f 100644 --- a/src/features/Main/index.jsx +++ b/src/features/Main/index.jsx @@ -67,12 +67,12 @@ const Main = () => { { path: '/instructors', component: InstructorsPage, exact: true }, { path: '/instructors/:instructorUsername', component: InstructorsDetailPage, exact: true }, { path: '/courses', component: CoursesPage, exact: true }, - { path: '/courses/:courseName', component: CoursesDetailPage, exact: true }, - { path: '/courses/:courseName/:className', component: ClassPage, exact: true }, + { path: '/courses/:courseId', component: CoursesDetailPage, exact: true }, + { path: '/courses/:courseId/:classId', component: ClassPage, exact: true }, { path: '/licenses', component: LicensesPage, exact: true }, { path: '/licenses/:licenseId', component: LicensesDetailPage, exact: true }, { path: '/classes', component: ClassesPage, exact: true }, - { path: '/manage-instructors/:courseName/:className', component: ManageInstructors, exact: true }, + { path: '/manage-instructors/:courseId/:classId', component: ManageInstructors, exact: true }, ]; return ( diff --git a/src/features/Students/StudentsTable/columns.jsx b/src/features/Students/StudentsTable/columns.jsx index 43bd7b05..8997b2fe 100644 --- a/src/features/Students/StudentsTable/columns.jsx +++ b/src/features/Students/StudentsTable/columns.jsx @@ -26,7 +26,7 @@ const columns = [ accessor: 'className', Cell: ({ row }) => ( {row.values.className}