From a81fe7efbb6d83b1fe0b5f9150cc4a446b14fabe 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 key --- .../Classes/Class/ClassPage/Actions.jsx | 18 ++++--- .../Classes/Class/ClassPage/index.jsx | 36 +++++++++----- src/features/Classes/ClassesTable/columns.jsx | 2 +- src/features/Classes/EnrollStudent/index.jsx | 28 ++++++----- src/features/Classes/InstructorCard/index.jsx | 25 ++++------ src/features/Classes/data/thunks.js | 12 ++--- src/features/Common/data/api.js | 6 +-- .../Courses/CourseDetailTable/columns.jsx | 4 +- .../Courses/CoursesDetailPage/index.jsx | 15 +++--- src/features/Courses/CoursesTable/columns.jsx | 2 +- .../ManageInstructors/ListInstructors.jsx | 11 ++--- .../Instructors/ManageInstructors/index.jsx | 48 ++++++++++++++----- src/features/Main/index.jsx | 6 +-- 13 files changed, 123 insertions(+), 90 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..0bd7cdf7 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} 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/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/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/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/CourseDetailTable/columns.jsx b/src/features/Courses/CourseDetailTable/columns.jsx index 834a9c16..0471af8c 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} 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/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/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/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/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 (