Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PADV-1554 - Change course/class title by a unique identifier (course_id/class_id) #112

Merged
merged 1 commit into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions src/features/Classes/Class/ClassPage/Actions.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand All @@ -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 (
Expand All @@ -51,7 +49,7 @@ const Actions = ({ previousPage }) => {
</Button>
<Button
as="a"
onClick={() => setAssignStaffRole(classLink, queryClassId)}
onClick={() => setAssignStaffRole(classLink, classIdDecoded)}
className="text-decoration-none text-white button-view-class mr-3"
>
<i className="fa-solid fa-arrow-up-right-from-square mr-2 mb-1" />
Expand Down Expand Up @@ -93,7 +91,7 @@ const Actions = ({ previousPage }) => {
isEditing
isDetailClassPage
/>
<EnrollStudent isOpen={isEnrollModalOpen} onClose={handleEnrollStudentModal} queryClassId={queryClassId} />
<EnrollStudent isOpen={isEnrollModalOpen} onClose={handleEnrollStudentModal} />
</>
);
};
Expand Down
12 changes: 6 additions & 6 deletions src/features/Classes/Class/ClassPage/__test__/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ const mockStore = {
{
learnerName: 'Test User',
learnerEmail: '[email protected]',
courseId: 'course-v1:demo+demo1+2020',
courseId: 'course-v1:XXX+YYY+2023',
courseName: 'Demo Course 1',
classId: 'ccx-v1:demo+demo1+2020+ccx@3',
classId: 'ccx-v1:XXX+YYY+2023+ccx@111',
className: 'test ccx1',
created: '2024-02-13T18:31:27.399407Z',
status: 'Active',
Expand All @@ -48,8 +48,8 @@ const mockStore = {
describe('ClassesPage', () => {
test('renders classes data and pagination', async () => {
const component = renderWithProviders(
<MemoryRouter initialEntries={['/courses/Demo%20Course%201/test%20ccx1?classId=ccx-v1:demo+demo1+2020+ccx@3']}>
<Route path="/courses/:courseName/:className">
<MemoryRouter initialEntries={[`/courses/${encodeURIComponent('course-v1:XXX+YYY+2023')}/${encodeURIComponent('ccx-v1:XXX+YYY+2023+ccx@111')}`]}>
<Route path="/courses/:courseId/:classId">
<ClassPage />
</Route>
</MemoryRouter>,
Expand All @@ -71,8 +71,8 @@ describe('ClassesPage', () => {

test('renders actions', async () => {
const { getByText, getByTestId } = renderWithProviders(
<MemoryRouter initialEntries={['/courses/Demo%20Course%201/test%20ccx1?classId=ccx-v1:demo+demo1+2020+ccx@3']}>
<Route path="/courses/:courseName/:className">
<MemoryRouter initialEntries={[`/courses/${encodeURIComponent('course-v1:XXX+YYY+2023')}/${encodeURIComponent('ccx-v1:XXX+YYY+2023+ccx@111')}`]}>
<Route path="/courses/:courseId/:classId">
<ClassPage />
</Route>
</MemoryRouter>,
Expand Down
36 changes: 24 additions & 12 deletions src/features/Classes/Class/ClassPage/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import React, { useState, useEffect, useRef } from 'react';
import React, {
useState,
useEffect,
useRef,
useMemo,
} from 'react';
import { useParams, useHistory, useLocation } from 'react-router-dom';
import { Container, Pagination } from '@edx/paragon';
import { useDispatch, useSelector } from 'react-redux';
Expand All @@ -25,11 +30,11 @@ const ClassPage = () => {
const location = useLocation();
const history = useHistory();
const dispatch = useDispatch();
const { courseName, className } = useParams();
const { courseId, classId } = useParams();
const queryParams = new URLSearchParams(location.search);
const previousPage = queryParams.get('previous') || 'classes';
const courseNameDecoded = decodeURIComponent(courseName);
const classNameDecoded = decodeURIComponent(className);
const courseIdDecoded = decodeURIComponent(courseId);
const classIdDecoded = decodeURIComponent(classId);

const institutionRef = useRef(undefined);
const [currentPage, setCurrentPage] = useState(initialPage);
Expand All @@ -44,23 +49,30 @@ const ClassPage = () => {
dispatch(updateCurrentPage(targetPage));
};

const defaultClassInfo = useMemo(() => ({
className: '',
}), []);

const classInfo = useSelector((state) => state.classes.allClasses.data)
.find((classElement) => classElement?.classId === classIdDecoded) || defaultClassInfo;

useEffect(() => {
const initialTitle = document.title;

document.title = classNameDecoded;
document.title = classIdDecoded;
// Leaves a gap time space to prevent being override by ActiveTabUpdater component
setTimeout(() => dispatch(updateActiveTab(previousPage)), 100);

return () => {
document.title = initialTitle;
};
}, [dispatch, classNameDecoded, previousPage]);
}, [dispatch, classIdDecoded, previousPage]);

useEffect(() => {
if (institution.id) {
const params = {
course_name: courseNameDecoded,
class_name: classNameDecoded,
course_id: courseIdDecoded,
class_id: classIdDecoded,
limit: true,
};

Expand All @@ -71,18 +83,18 @@ const ClassPage = () => {
dispatch(resetStudentsTable());
dispatch(updateCurrentPage(initialPage));
};
}, [dispatch, institution.id, courseNameDecoded, classNameDecoded, currentPage]);
}, [dispatch, institution.id, courseIdDecoded, classIdDecoded, currentPage]);

useEffect(() => {
if (institution.id) {
dispatch(fetchAllClassesData(institution.id, courseNameDecoded));
dispatch(fetchAllClassesData(institution.id, courseIdDecoded));
}

return () => {
dispatch(resetClassesTable());
dispatch(resetClasses());
};
}, [dispatch, institution.id, courseNameDecoded]);
}, [dispatch, institution.id, courseIdDecoded]);

useEffect(() => {
if (institution.id !== undefined && institutionRef.current === undefined) {
Expand All @@ -102,7 +114,7 @@ const ClassPage = () => {
<Button onClick={() => history.goBack()} className="mr-3 link back-arrow" variant="tertiary">
<i className="fa-solid fa-arrow-left" />
</Button>
<h3 className="h2 mb-0 course-title">Class details: {classNameDecoded}</h3>
<h3 className="h2 mb-0 course-title">Class details: {classInfo.className}</h3>
</div>
</div>

Expand Down
4 changes: 2 additions & 2 deletions src/features/Classes/ClassesTable/columns.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const columns = [
accessor: 'className',
Cell: ({ row }) => (
<LinkWithQuery
to={`/courses/${encodeURIComponent(row.original.masterCourseName)}/${encodeURIComponent(row.values.className)}?classId=${row.original.classId}&previous=classes`}
to={`/courses/${encodeURIComponent(row.original.masterCourseId)}/${encodeURIComponent(row.original.classId)}?previous=classes`}
className="text-truncate link"
>
{row.values.className}
Expand Down Expand Up @@ -125,7 +125,7 @@ const columns = [
</Dropdown.Item>
<Dropdown.Item>
<LinkWithQuery
to={`/manage-instructors/${encodeURIComponent(masterCourseName)}/${encodeURIComponent(row.values.className)}?classId=${classId}&previous=classes`}
to={`/manage-instructors/${encodeURIComponent(masterCourseId)}/${encodeURIComponent(classId)}?previous=classes`}
className="text-truncate text-decoration-none custom-text-black"
>
<i className="fa-regular fa-chalkboard-user mr-2 mb-1" />
Expand Down
32 changes: 22 additions & 10 deletions src/features/Classes/EnrollStudent/__test__/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -21,15 +20,28 @@ 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();
});

test('Should render with correct elements', () => {
const { getByText, getByPlaceholderText } = renderWithProviders(
<EnrollStudent isOpen onClose={() => {}} queryClassId="ccx1" />,
{ preloadedState: {} },
<EnrollStudent isOpen onClose={() => {}} />,
{ preloadedState: mockStore },
);

expect(getByText('Invite student to enroll')).toBeInTheDocument();
Expand All @@ -42,8 +54,8 @@ describe('EnrollStudent', () => {
const onCloseMock = jest.fn();

const { getByPlaceholderText, getByText, getByTestId } = renderWithProviders(
<EnrollStudent isOpen onClose={onCloseMock} queryClassId="ccx1" />,
{ preloadedState: {} },
<EnrollStudent isOpen onClose={onCloseMock} />,
{ preloadedState: mockStore },
);

const handleEnrollmentsMock = jest.spyOn(api, 'handleEnrollments').mockResolvedValue({
Expand Down Expand Up @@ -78,8 +90,8 @@ describe('EnrollStudent', () => {
});

const { getByPlaceholderText, getByText } = renderWithProviders(
<EnrollStudent isOpen onClose={onCloseMock} queryClassId="ccx1" />,
{ preloadedState: {} },
<EnrollStudent isOpen onClose={onCloseMock} />,
{ preloadedState: mockStore },
);

const emailInput = getByPlaceholderText('Enter email of the student to enroll');
Expand All @@ -101,8 +113,8 @@ describe('EnrollStudent', () => {
const onCloseMock = jest.fn();

const { getByPlaceholderText, getByText, getByTestId } = renderWithProviders(
<EnrollStudent isOpen onClose={onCloseMock} queryClassId="ccx1" />,
{ preloadedState: {} },
<EnrollStudent isOpen onClose={onCloseMock} />,
{ preloadedState: mockStore },
);

const handleEnrollmentsMock = jest.spyOn(api, 'handleEnrollments').mockResolvedValue({
Expand Down
28 changes: 17 additions & 11 deletions src/features/Classes/EnrollStudent/index.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();
Expand All @@ -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 = [];
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -125,7 +132,7 @@ const EnrollStudent = ({ isOpen, onClose, queryClassId }) => {
</ModalDialog.Header>
<ModalDialog.Body className="body-container h-100">
<p className="text-uppercase font-weight-bold sub-title">
Class: {classNameDecoded}
Class: {classInfo.className}
</p>
{isLoading && (
<div className="w-100 h-100 d-flex justify-content-center align-items-center">
Expand Down Expand Up @@ -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;
8 changes: 4 additions & 4 deletions src/features/Classes/InstructorCard/__test__/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -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,
Expand Down
Loading
Loading