diff --git a/src/assets/colors.scss b/src/assets/colors.scss
index 9ed2a4ca..35bce831 100644
--- a/src/assets/colors.scss
+++ b/src/assets/colors.scss
@@ -1,20 +1,28 @@
-// Color variables
+// Primary
$primary: #007394;
$primary-dark: #003057;
+
+// Basic Colors
$color-black: #020917;
-$black-80: #333;
$color-white: #fefefe;
$color-gray: #60646d;
-$gray-70: #666666;
-$gray-60: #808080;
-$gray-30: #d3d3d3;
-$gray-20: #dfe1e1;
$color-pink: #ffecf0;
$color-green: #e7f7eb;
$color-yellow: #fafbe5;
$color-purple: #f4e3ee;
-$blue-20: #e4faff;
-
-$hyperlink-color: #17897c;
$color-active-button: #989ba3;
+$hyperlink-color: #17897c;
$bg-main-color: #f3f3f3;
+
+// Gray
+$gray-20: #dfe1e1;
+$gray-30: #d3d3d3;
+$gray-60: #808080;
+$gray-70: #666666;
+
+// Blue
+$blue-20: #e4faff;
+
+// Black
+$black: #000;
+$black-80: #333;
diff --git a/src/assets/global.scss b/src/assets/global.scss
index 82b2647b..f3f747f3 100644
--- a/src/assets/global.scss
+++ b/src/assets/global.scss
@@ -1,8 +1,9 @@
@import "assets/colors.scss";
+@import "assets/variables.scss";
.page-content-container {
border: 1px solid $gray-20;
- border-radius: 0.375rem;
+ border-radius: $border-radius-1;
padding: 20px 0;
box-shadow: 0px 3px 12px 0px $gray-30;
background-color: $color-white;
@@ -17,7 +18,7 @@
.filter-container .filters {
border: 1px solid $gray-20;
- border-radius: 0.375rem;
+ border-radius: $border-radius-1;
padding: 1.5rem 1rem;
}
@@ -34,3 +35,9 @@
list-style-type: none;
padding-left: 0;
}
+
+.link {
+ &:-webkit-any-link {
+ color: $primary;
+ }
+}
diff --git a/src/assets/variables.scss b/src/assets/variables.scss
index c4975236..15700467 100644
--- a/src/assets/variables.scss
+++ b/src/assets/variables.scss
@@ -2,3 +2,5 @@
$font-family: "Open Sans", sans-serif;
$account-menu-box-shadow: 2px 3px 8px 0 rgba(0, 0, 0, 0.5);
+
+$border-radius-1: 0.375rem;
diff --git a/src/features/Classes/data/thunks.js b/src/features/Classes/data/thunks.js
index 0091b763..1cc25b3a 100644
--- a/src/features/Classes/data/thunks.js
+++ b/src/features/Classes/data/thunks.js
@@ -7,12 +7,12 @@ import {
} from 'features/Classes/data/slice';
import { getClassesByInstitution } from 'features/Common/data/api';
-function fetchClassesData(id, currentPage) {
+function fetchClassesData(id, currentPage, courseName = '') {
return async (dispatch) => {
dispatch(fetchClassesDataRequest);
try {
- const response = camelCaseObject(await getClassesByInstitution(id, '', true, '', currentPage));
+ const response = camelCaseObject(await getClassesByInstitution(id, courseName, true, '', currentPage));
dispatch(fetchClassesDataSuccess(response.data));
} catch (error) {
dispatch(fetchClassesDataFailed());
diff --git a/src/features/Common/data/api.js b/src/features/Common/data/api.js
index 9bb5f8e8..47bed09c 100644
--- a/src/features/Common/data/api.js
+++ b/src/features/Common/data/api.js
@@ -21,7 +21,6 @@ function getLicensesByInstitution(institutionId, limit, page = 1, urlParamsFilte
function getClassesByInstitution(institutionId, courseName, limit = false, instructorsList = '', page = '') {
const encodedCourseName = encodeURIComponent(courseName);
-
return getAuthenticatedHttpClient().get(
`${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/classes`
+ `/?limit=${limit}&institution_id=${institutionId}&course_name=${encodedCourseName}&instructors=${instructorsList}&page=${page}`,
diff --git a/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx b/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx
new file mode 100644
index 00000000..12374c7e
--- /dev/null
+++ b/src/features/Courses/CourseDetailTable/__test__/columns.test.jsx
@@ -0,0 +1,202 @@
+import {
+ fireEvent,
+ waitFor,
+} from '@testing-library/react';
+import '@testing-library/jest-dom/extend-expect';
+import { MemoryRouter, Route } from 'react-router-dom';
+
+import { renderWithProviders } from 'test-utils';
+import { columns } from 'features/Courses/CourseDetailTable/columns';
+
+describe('columns', () => {
+ test('returns an array of columns with correct properties', () => {
+ expect(columns).toBeInstanceOf(Array);
+ expect(columns).toHaveLength(7);
+
+ const [
+ className,
+ instructor,
+ enrollmentStatus,
+ studentsEnrolled,
+ maxStudents,
+ startDate,
+ endDate,
+ ] = columns;
+
+ expect(className).toHaveProperty('Header', 'Class');
+ expect(className).toHaveProperty('accessor', 'className');
+
+ expect(instructor).toHaveProperty('Header', 'Instructor');
+ expect(instructor).toHaveProperty('accessor', 'instructors');
+
+ expect(enrollmentStatus).toHaveProperty('Header', 'Enrollment status');
+ expect(enrollmentStatus).toHaveProperty('accessor', 'numberOfPendingStudents');
+
+ expect(studentsEnrolled).toHaveProperty('Header', 'Students Enrolled');
+ expect(studentsEnrolled).toHaveProperty('accessor', 'numberOfStudents');
+
+ expect(maxStudents).toHaveProperty('Header', 'Max');
+ expect(maxStudents).toHaveProperty('accessor', 'maxStudents');
+
+ expect(startDate).toHaveProperty('Header', 'Start date');
+ expect(startDate).toHaveProperty('accessor', 'startDate');
+
+ expect(endDate).toHaveProperty('Header', 'End date');
+ expect(endDate).toHaveProperty('accessor', 'endDate');
+ });
+
+ test('Should render the title into a span tag', () => {
+ const title = columns[0].Cell({ row: { values: { className: 'Class example' } } });
+ expect(title).toHaveProperty('type', 'span');
+ expect(title.props).toEqual({ className: 'text-truncate', children: 'Class example' });
+ });
+
+ test('Should render the dates', () => {
+ const startDate = columns[5].Cell({ row: { values: { startDate: '2024-02-13T17:42:22Z' } } });
+ expect(startDate).toBe('02/13/24');
+
+ const endDate = columns[5].Cell({ row: { values: { startDate: '2024-04-13T17:42:22Z' } } });
+ expect(endDate).toBe('04/13/24');
+
+ const nullDate = columns[5].Cell({ row: { values: { startDate: null } } });
+ expect(nullDate).toBe('-');
+
+ const nullDate2 = columns[6].Cell({ row: { values: { startDate: null } } });
+ expect(nullDate2).toBe('-');
+ });
+
+ test('Should render the enrollment status', () => {
+ const pendingStudents = { row: { values: { numberOfStudents: 3, numberOfPendingStudents: 1 } } };
+
+ const enrollmentStatus = columns[2].Cell(pendingStudents);
+ expect(enrollmentStatus.props).toEqual({ children: ['Pending (', 1, ')'], light: true, variant: 'warning' });
+
+ const completeStudents = { row: { values: { numberOfStudents: 3, numberOfPendingStudents: 0 } } };
+
+ const enrollmentStatusComplete = columns[2].Cell(completeStudents);
+ expect(enrollmentStatusComplete.props).toEqual({ children: 'Complete', light: true, variant: 'success' });
+ });
+
+ test('Should render the students enrolled', () => {
+ const values = { row: { values: { numberOfStudents: 3, numberOfPendingStudents: 1 } } };
+
+ const studentsEnrolled = columns[3].Cell(values);
+ expect(studentsEnrolled).toHaveProperty('type', 'span');
+ expect(studentsEnrolled.props).toEqual({ children: 3 });
+ });
+
+ test('Should render the instructors', () => {
+ const values = {
+ row: {
+ values: { instructors: ['Sam Sepiol'] },
+ original: {
+ classId: 'Demo Course 1',
+ },
+ },
+ };
+
+ const Component = () => columns[1].Cell(values);
+ const mockStore = {
+ classes: {
+ table: {
+ data: [
+ {
+ masterCourseName: 'Demo MasterCourse 1',
+ className: 'Demo Class 1',
+ startDate: '09/21/24',
+ endDate: null,
+ numberOfStudents: 1,
+ maxStudents: 100,
+ instructors: ['Sam Sepiol'],
+ },
+ {
+ masterCourseName: 'Demo MasterCourse 2',
+ className: 'Demo Class 2',
+ startDate: '09/21/25',
+ endDate: null,
+ numberOfStudents: 2,
+ maxStudents: 200,
+ instructors: [],
+ },
+ ],
+ count: 2,
+ num_pages: 1,
+ current_page: 1,
+ },
+ },
+ };
+
+ const component = renderWithProviders(
+
+
+
+
+ ,
+ { preloadedState: mockStore },
+ );
+
+ expect(component.getByText('Sam Sepiol')).toBeInTheDocument();
+ });
+
+ test('Should render the assign button if instructor is not present', async () => {
+ const values = {
+ row: {
+ values: { instructors: [] },
+ original: {
+ classId: 'Demo Course 1',
+ },
+ },
+ };
+
+ const ComponentNoInstructor = () => columns[1].Cell(values);
+
+ const mockStore = {
+ classes: {
+ table: {
+ data: [
+ {
+ masterCourseName: 'Demo MasterCourse 1',
+ className: 'Demo Class 1',
+ startDate: '09/21/24',
+ endDate: null,
+ numberOfStudents: 1,
+ maxStudents: 100,
+ instructors: ['Sam Sepiol'],
+ },
+ {
+ masterCourseName: 'Demo MasterCourse 2',
+ className: 'Demo Class 2',
+ startDate: '09/21/25',
+ endDate: null,
+ numberOfStudents: 2,
+ maxStudents: 200,
+ instructors: [],
+ },
+ ],
+ count: 2,
+ num_pages: 1,
+ current_page: 1,
+ },
+ },
+ };
+
+ const component = renderWithProviders(
+
+
+
+
+ ,
+ { preloadedState: mockStore },
+ );
+
+ const modalButton = component.getByRole('button');
+ expect(modalButton).toBeInTheDocument();
+
+ fireEvent.click(modalButton);
+
+ await waitFor(() => {
+ const title = component.getAllByText('Assign instructor')[0];
+ expect(title).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/features/Courses/CourseDetailTable/__test__/index.test.jsx b/src/features/Courses/CourseDetailTable/__test__/index.test.jsx
new file mode 100644
index 00000000..32c0c52c
--- /dev/null
+++ b/src/features/Courses/CourseDetailTable/__test__/index.test.jsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import '@testing-library/jest-dom';
+import { MemoryRouter, Route } from 'react-router-dom';
+import { render, screen } from '@testing-library/react';
+
+import { renderWithProviders } from 'test-utils';
+import CourseDetailTable from 'features/Courses/CourseDetailTable';
+import { columns } from 'features/Courses/CourseDetailTable/columns';
+
+describe('Course Details Table', () => {
+ test('Should render the table without data', () => {
+ render();
+ const emptyTableText = screen.getByText('No classes were found.');
+ expect(emptyTableText).toBeInTheDocument();
+ });
+
+ test('Should render the table with data', () => {
+ const mockStore = {
+ classes: {
+ table: {
+ data: [
+ {
+ masterCourseName: 'Demo MasterCourse 1',
+ className: 'Demo Class 1',
+ startDate: '09/21/24',
+ endDate: null,
+ numberOfStudents: 1,
+ maxStudents: 100,
+ instructors: ['instructor_1'],
+ },
+ {
+ masterCourseName: 'Demo MasterCourse 2',
+ className: 'Demo Class 2',
+ startDate: '09/21/25',
+ endDate: null,
+ numberOfStudents: 2,
+ maxStudents: 200,
+ instructors: ['instructor_2'],
+ },
+ ],
+ count: 2,
+ num_pages: 1,
+ current_page: 1,
+ },
+ },
+ };
+
+ const component = renderWithProviders(
+
+
+
+
+ ,
+ { preloadedState: mockStore },
+ );
+
+ expect(component.container).toHaveTextContent('Class');
+ expect(component.container).toHaveTextContent('Instructor');
+ expect(component.container).toHaveTextContent('Enrollment status');
+ expect(component.container).toHaveTextContent('Students Enrolled');
+ expect(component.container).toHaveTextContent('Max');
+ expect(component.container).toHaveTextContent('Start date');
+ expect(component.container).toHaveTextContent('End date');
+ });
+});
diff --git a/src/features/Courses/CourseDetailTable/columns.jsx b/src/features/Courses/CourseDetailTable/columns.jsx
new file mode 100644
index 00000000..b86a164e
--- /dev/null
+++ b/src/features/Courses/CourseDetailTable/columns.jsx
@@ -0,0 +1,102 @@
+/* eslint-disable react/prop-types */
+import React, { useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useParams } from 'react-router-dom';
+import { format } from 'date-fns';
+
+import { Badge, Button } from 'react-paragon-topaz';
+import AssignInstructors from 'features/Instructors/AssignInstructors';
+
+import { fetchClassesData } from 'features/Classes/data/thunks';
+import { updateClassSelected } from 'features/Instructors/data/slice';
+
+import { initialPage } from 'features/constants';
+
+const columns = [
+ {
+ Header: 'Class',
+ accessor: 'className',
+ Cell: ({ row }) => ({row.values.className}),
+ },
+ {
+ Header: 'Instructor',
+ accessor: 'instructors',
+ Cell: ({ row }) => {
+ const { classId } = useParams();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const dispatch = useDispatch();
+ const institution = useSelector((state) => state.main.selectedInstitution);
+
+ const handleOpenModal = () => {
+ dispatch(updateClassSelected(row.original.classId));
+ setIsModalOpen(true);
+ };
+
+ const handleCloseModal = () => {
+ dispatch(fetchClassesData(institution.id, initialPage, classId));
+ setIsModalOpen(false);
+ };
+
+ if (row.values.instructors?.length > 0) {
+ return (
+
+ {row.values.instructors.map(instructor => - {`${instructor}`}
)}
+
+ );
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+ },
+ },
+ {
+ Header: 'Enrollment status',
+ accessor: 'numberOfPendingStudents',
+ Cell: ({ row }) => {
+ const isEnrollmentComplete = row.values.numberOfPendingStudents === 0;
+
+ return isEnrollmentComplete ? (
+ Complete
+ ) : (
+ Pending ({row.values.numberOfPendingStudents})
+ );
+ },
+ },
+ {
+ Header: 'Students Enrolled',
+ accessor: 'numberOfStudents',
+ Cell: ({ row }) => (
+
+ {row.values.numberOfStudents}
+
+ ),
+ },
+ {
+ Header: 'Max',
+ accessor: 'maxStudents',
+ },
+ {
+ Header: 'Start date',
+ accessor: 'startDate',
+ Cell: ({ row }) => (row.values.startDate ? format(row.values.startDate, 'MM/dd/yy') : '-'),
+ },
+ {
+ Header: 'End date',
+ accessor: 'endDate',
+ Cell: ({ row }) => (row.values.endDate ? format(row.values.endDate, 'MM/dd/yy') : '-'),
+ },
+];
+
+export { columns };
diff --git a/src/features/Courses/CourseDetailTable/index.jsx b/src/features/Courses/CourseDetailTable/index.jsx
new file mode 100644
index 00000000..b585bca4
--- /dev/null
+++ b/src/features/Courses/CourseDetailTable/index.jsx
@@ -0,0 +1,37 @@
+import React, { useMemo } from 'react';
+import PropTypes from 'prop-types';
+import { IntlProvider } from 'react-intl';
+import DataTable from '@edx/paragon/dist/DataTable';
+
+import { columns } from 'features/Courses/CourseDetailTable/columns';
+
+const CourseDetailTable = ({ data, count }) => {
+ const COLUMNS = useMemo(() => columns, []);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+CourseDetailTable.propTypes = {
+ data: PropTypes.arrayOf(PropTypes.shape([])),
+ count: PropTypes.number,
+};
+
+CourseDetailTable.defaultProps = {
+ data: [],
+ count: 0,
+};
+
+export default CourseDetailTable;
diff --git a/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx b/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx
new file mode 100644
index 00000000..9e026087
--- /dev/null
+++ b/src/features/Courses/CoursesDetailPage/__test__/index.test.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import { waitFor } from '@testing-library/react';
+import { MemoryRouter, Route } from 'react-router-dom';
+import '@testing-library/jest-dom/extend-expect';
+
+import { renderWithProviders } from 'test-utils';
+import CoursesDetailPage from 'features/Courses/CoursesDetailPage';
+
+jest.mock('@edx/frontend-platform/logging', () => ({
+ logError: jest.fn(),
+}));
+
+const mockStore = {
+ main: {
+ selectedInstitution: {
+ id: 1,
+ name: 'Institution 1',
+ shortName: 'Test',
+ active: true,
+ externalId: '',
+ created: '2023-06-22T22:48:56.124907Z',
+ modified: '2023-06-22T22:48:56.124907Z',
+ label: 'Institution 1',
+ value: 1,
+ },
+ },
+ courses: {
+ table: {
+ data: [
+ {
+ masterCourseName: 'Demo Course 1',
+ numberOfClasses: 3,
+ missingClassesForInstructor: 0,
+ numberOfStudents: 3,
+ numberOfPendingStudents: 0,
+ },
+ {
+ masterCourseName: 'Demo Course 2',
+ numberOfClasses: 3,
+ missingClassesForInstructor: 0,
+ numberOfStudents: 3,
+ numberOfPendingStudents: 0,
+ },
+ ],
+ count: 2,
+ num_pages: 1,
+ current_page: 1,
+ },
+ },
+ classes: {
+ table: {
+ data: [
+ {
+ masterCourseName: 'Demo MasterCourse 1',
+ className: 'Demo Class 1',
+ startDate: '09/21/24',
+ endDate: null,
+ numberOfStudents: 1,
+ maxStudents: 100,
+ instructors: ['instructor_1'],
+ },
+ {
+ masterCourseName: 'Demo MasterCourse 2',
+ className: 'Demo Class 2',
+ startDate: '09/21/25',
+ endDate: null,
+ numberOfStudents: 2,
+ maxStudents: 200,
+ instructors: ['instructor_2'],
+ },
+ ],
+ count: 2,
+ num_pages: 1,
+ current_page: 1,
+ },
+ },
+};
+
+describe('CoursesDetailPage', () => {
+ test('Should render the table and the course info', async () => {
+ const component = renderWithProviders(
+
+
+
+
+ ,
+ { preloadedState: mockStore },
+ );
+
+ waitFor(() => {
+ expect(component.container).toHaveTextContent('Demo Course 1 1');
+ expect(component.container).toHaveTextContent('Demo MasterCourse 1');
+ expect(component.container).toHaveTextContent('Demo MasterCourse 2');
+ expect(component.container).toHaveTextContent('Demo Class 1');
+ expect(component.container).toHaveTextContent('Demo Class 2');
+ expect(component.container).toHaveTextContent('09/21/24');
+ expect(component.container).toHaveTextContent('09/21/25');
+ expect(component.container).toHaveTextContent('1');
+ expect(component.container).toHaveTextContent('2');
+ expect(component.container).toHaveTextContent('100');
+ expect(component.container).toHaveTextContent('200');
+ expect(component.container).toHaveTextContent('instructor_1');
+ expect(component.container).toHaveTextContent('instructor_2');
+ });
+ });
+});
diff --git a/src/features/Courses/CoursesDetailPage/index.jsx b/src/features/Courses/CoursesDetailPage/index.jsx
new file mode 100644
index 00000000..729b3664
--- /dev/null
+++ b/src/features/Courses/CoursesDetailPage/index.jsx
@@ -0,0 +1,111 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Link, useParams, useHistory } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { Container, Pagination } from '@edx/paragon';
+import CourseDetailTable from 'features/Courses/CourseDetailTable';
+
+import { fetchClassesData } from 'features/Classes/data/thunks';
+import { fetchCoursesData } from 'features/Courses/data/thunks';
+import { fetchClassesDataSuccess } from 'features/Classes/data/slice';
+import { fetchCoursesDataSuccess, updateCurrentPage } from 'features/Courses/data/slice';
+import { initialPage } from 'features/constants';
+
+import 'features/Courses/CoursesDetailPage/index.scss';
+
+const CoursesDetailPage = () => {
+ const history = useHistory();
+ const dispatch = useDispatch();
+ const { classId } = useParams();
+
+ const institutionRef = useRef(undefined);
+ const [currentPage, setCurrentPage] = useState(initialPage);
+
+ const defaultCourseInfo = {
+ numberOfStudents: '-',
+ numberOfPendingStudents: '-',
+ };
+
+ const courseInfo = useSelector((state) => state.courses.table.data)
+ .find((course) => course?.masterCourseName === classId) || defaultCourseInfo;
+ const institution = useSelector((state) => state.main.selectedInstitution);
+ const classes = useSelector((state) => state.classes.table);
+ const totalStudents = courseInfo.numberOfStudents + courseInfo.numberOfPendingStudents;
+
+ const handlePagination = (targetPage) => {
+ setCurrentPage(targetPage);
+ dispatch(updateCurrentPage(targetPage));
+ };
+
+ useEffect(() => {
+ const initialState = {
+ results: [],
+ count: 0,
+ numPages: 0,
+ };
+
+ if (institution.id) {
+ dispatch(fetchClassesData(institution.id, initialPage, classId));
+ dispatch(fetchCoursesData(institution.id, initialPage, null));
+ }
+
+ return () => {
+ dispatch(fetchCoursesDataSuccess(initialState));
+ dispatch(fetchClassesDataSuccess(initialState));
+ };
+ }, [dispatch, institution.id, classId]);
+
+ useEffect(() => {
+ if (institution.id !== undefined && institutionRef.current === undefined) {
+ institutionRef.current = institution.id;
+ }
+
+ if (institution.id !== institutionRef.current) {
+ history.push('/courses');
+ }
+ }, [institution, history]);
+
+ return (
+
+
+
+
+
+
+
{classId}
+
+
+
+
+
Students enrolled
+
+ {courseInfo.numberOfStudents}
+ {' / '}
+ {totalStudents}
+
+
+
+
+
Classes
+
{classes.count}
+
+
+
+
+
+ {classes.numPages > 1 && (
+
+ )}
+
+ );
+};
+
+export default CoursesDetailPage;
diff --git a/src/features/Courses/CoursesDetailPage/index.scss b/src/features/Courses/CoursesDetailPage/index.scss
new file mode 100644
index 00000000..a58c3671
--- /dev/null
+++ b/src/features/Courses/CoursesDetailPage/index.scss
@@ -0,0 +1,55 @@
+@import 'assets/variables.scss';
+@import 'assets/colors.scss';
+
+.course-title {
+ max-width: 700px;
+}
+
+.card-container {
+ height: 100px;
+ padding: 0 1.5rem;
+ padding-left: 2rem;
+ padding-right: 4rem;
+ background-color: $color-white;
+ border-radius: $border-radius-1;
+ box-shadow: 0px 3px 12px 0px rgba(0, 0, 0, 0.16);
+
+ .title {
+ font-size: 15px;
+ font-weight: 700;
+ color: $gray-70;
+ margin-bottom: 5px;
+ text-transform: uppercase;
+ }
+
+ .value {
+ display: block;
+ width: 100%;
+ color: $black;
+ }
+
+ .separator {
+ width: 2px;
+ height: 50px;
+ background-color: $gray-20;
+ }
+}
+
+.btntpz.button-assign.btn.btn-primary {
+ height: 36px;
+ background-color: $color-white;
+ color: $primary;
+ border-width: 2px;
+}
+
+@media screen and (max-width: 576px) {
+ .card-container {
+ padding: 0 1.5rem;
+ height: 115px;
+ padding-right: 4rem;
+
+ .title {
+ margin-bottom: 5px;
+ }
+ }
+}
diff --git a/src/features/Courses/CoursesPage/_test_/index.test.jsx b/src/features/Courses/CoursesPage/_test_/index.test.jsx
index d1f48a60..52bda8d1 100644
--- a/src/features/Courses/CoursesPage/_test_/index.test.jsx
+++ b/src/features/Courses/CoursesPage/_test_/index.test.jsx
@@ -1,8 +1,10 @@
import React from 'react';
-import CoursesPage from 'features/Courses/CoursesPage';
import { waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
+import { MemoryRouter, Route } from 'react-router-dom';
+
import { renderWithProviders } from 'test-utils';
+import CoursesPage from 'features/Courses/CoursesPage';
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
@@ -37,7 +39,11 @@ const mockStore = {
describe('CoursesPage', () => {
it('renders courses data and pagination', async () => {
const component = renderWithProviders(
- ,
+
+
+
+
+ ,
{ preloadedState: mockStore },
);
diff --git a/src/features/Courses/CoursesTable/_test_/index.test.jsx b/src/features/Courses/CoursesTable/_test_/index.test.jsx
index d54dc2a5..cc956c69 100644
--- a/src/features/Courses/CoursesTable/_test_/index.test.jsx
+++ b/src/features/Courses/CoursesTable/_test_/index.test.jsx
@@ -1,5 +1,6 @@
import React from 'react';
import '@testing-library/jest-dom';
+import { MemoryRouter, Route } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import CoursesTable from 'features/Courses/CoursesTable';
@@ -12,7 +13,7 @@ describe('Courses Table', () => {
expect(emptyTableText).toBeInTheDocument();
});
- test('renders CoursessTable with data', () => {
+ test('renders CoursesTable with data', () => {
const data = [
{
masterCourseName: 'Demo Course 1',
@@ -31,7 +32,11 @@ describe('Courses Table', () => {
];
const component = render(
- ,
+
+
+
+
+ ,
);
// Check if the table rows are present
diff --git a/src/features/Courses/CoursesTable/columns.jsx b/src/features/Courses/CoursesTable/columns.jsx
index 424fd187..9a3b40f4 100644
--- a/src/features/Courses/CoursesTable/columns.jsx
+++ b/src/features/Courses/CoursesTable/columns.jsx
@@ -1,11 +1,13 @@
/* eslint-disable react/prop-types, no-nested-ternary */
import React from 'react';
+import { Link } from 'react-router-dom';
import { Badge } from 'react-paragon-topaz';
const columns = [
{
Header: 'Courses',
accessor: 'masterCourseName',
+ Cell: ({ row }) => ({row.values.masterCourseName}),
},
{
Header: 'Classes',
diff --git a/src/features/Dashboard/DashboardPage/index.scss b/src/features/Dashboard/DashboardPage/index.scss
index a440135e..7aaaa6b5 100644
--- a/src/features/Dashboard/DashboardPage/index.scss
+++ b/src/features/Dashboard/DashboardPage/index.scss
@@ -1,4 +1,5 @@
@import "assets/colors.scss";
+@import "assets/variables.scss";
.license-section {
background-color: $color-white;
@@ -12,7 +13,7 @@
.license-section,
.instructor-assign-section {
box-shadow: 0px 3px 12px 0px rgba(0, 0, 0, 0.16);
- border-radius: 0.375rem;
+ border-radius: $border-radius-1;
}
.schedule-section {
diff --git a/src/features/Dashboard/WeeklySchedule/index.scss b/src/features/Dashboard/WeeklySchedule/index.scss
index 099a22ad..4d1e87fb 100644
--- a/src/features/Dashboard/WeeklySchedule/index.scss
+++ b/src/features/Dashboard/WeeklySchedule/index.scss
@@ -1,10 +1,11 @@
@import "assets/colors.scss";
+@import "assets/variables.scss";
.header-schedule {
background-color: $primary;
padding: 1rem;
- border-top-right-radius: 0.375rem;
- border-top-left-radius: 0.375rem;
+ border-top-right-radius: $border-radius-1;
+ border-top-left-radius: $border-radius-1;
h3 {
color: $color-white;
@@ -14,7 +15,7 @@
.content-schedule {
box-shadow: 0px 3px 12px 0px rgba(0, 0, 0, 0.16);
- border-radius: 0.375rem;
+ border-radius: $border-radius-1;
.rdrDay:not(.rdrDayPassive) .rdrInRange ~ .rdrDayNumber span,
.rdrDay:not(.rdrDayPassive) .rdrStartEdge ~ .rdrDayNumber span,
@@ -41,7 +42,7 @@
}
.rdrCalendarWrapper {
- border-radius: 0.375rem;
+ border-radius: $border-radius-1;
}
.rdrMonthAndYearPickers select,
@@ -78,7 +79,7 @@
width: 100%;
background: $color-white;
padding: 0.8rem;
- border-radius: 0.375rem;
+ border-radius: $border-radius-1;
border-right: 1px solid $gray-20;
max-height: 330px;
overflow-y: auto;
diff --git a/src/features/Instructors/AddInstructors/index.jsx b/src/features/Instructors/AddInstructors/index.jsx
index a7ee0b2f..c73de9dc 100644
--- a/src/features/Instructors/AddInstructors/index.jsx
+++ b/src/features/Instructors/AddInstructors/index.jsx
@@ -1,5 +1,3 @@
-// This component will be modified according to the new wirefrime
-
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
diff --git a/src/features/Instructors/AssignInstructors/index.jsx b/src/features/Instructors/AssignInstructors/index.jsx
index 71fc9a51..a5003093 100644
--- a/src/features/Instructors/AssignInstructors/index.jsx
+++ b/src/features/Instructors/AssignInstructors/index.jsx
@@ -7,6 +7,7 @@ import { Button } from 'react-paragon-topaz';
import InstructorsFilters from 'features/Instructors/InstructorsFilters';
import AssignTable from 'features/Instructors/AssignInstructors/AssignTable';
+import { fetchClassesData as fetchClassesDataHome } from 'features/Dashboard/data';
import { fetchInstructorsData, assignInstructors } from 'features/Instructors/data';
import {
updateCurrentPage,
@@ -18,7 +19,7 @@ import {
import { initialPage } from 'features/constants';
import 'features/Instructors/AssignInstructors/index.scss';
-const AssignInstructors = ({ isOpen, close }) => {
+const AssignInstructors = ({ isOpen, close, getClasses }) => {
const dispatch = useDispatch();
const selectedInstitution = useSelector((state) => state.main.selectedInstitution);
const stateInstructors = useSelector((state) => state.instructors);
@@ -37,22 +38,32 @@ const AssignInstructors = ({ isOpen, close }) => {
};
const handleAssignInstructors = async () => {
- // eslint-disable-next-line array-callback-return
- rowsSelected.map(row => {
- const enrollmentData = new FormData();
- enrollmentData.append('unique_student_identifier', row);
- enrollmentData.append('rolename', 'staff');
- enrollmentData.append('action', 'allow');
- dispatch(assignInstructors(enrollmentData, classId, selectedInstitution.id));
- });
- close();
+ try {
+ const dispatchPromises = rowsSelected.map(row => {
+ const enrollmentData = new FormData();
+ enrollmentData.append('unique_student_identifier', row);
+ enrollmentData.append('rolename', 'staff');
+ enrollmentData.append('action', 'allow');
+ return dispatch(assignInstructors(enrollmentData, classId));
+ });
+
+ await Promise.all(dispatchPromises);
+
+ if (getClasses) {
+ dispatch(fetchClassesDataHome(selectedInstitution.id));
+ }
+ } finally {
+ close();
+ }
};
useEffect(() => {
if (Object.keys(selectedInstitution).length > 0) {
- dispatch(fetchInstructorsData(selectedInstitution.id, currentPage, stateInstructors.filters));
+ const instructorFilters = stateInstructors.filters;
+ dispatch(fetchInstructorsData(selectedInstitution.id, currentPage, instructorFilters));
}
- }, [currentPage, selectedInstitution, dispatch]); // eslint-disable-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [currentPage, selectedInstitution, dispatch]);
useEffect(() => {
if (!isOpen) {
@@ -107,6 +118,11 @@ const AssignInstructors = ({ isOpen, close }) => {
AssignInstructors.propTypes = {
isOpen: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
+ getClasses: PropTypes.bool,
+};
+
+AssignInstructors.defaultProps = {
+ getClasses: true,
};
export default AssignInstructors;
diff --git a/src/features/Instructors/data/slice.js b/src/features/Instructors/data/slice.js
index e6eb1265..2bf41b8f 100644
--- a/src/features/Instructors/data/slice.js
+++ b/src/features/Instructors/data/slice.js
@@ -66,14 +66,14 @@ export const instructorsSlice = createSlice({
updateClassSelected: (state, { payload }) => {
state.classSelected = payload;
},
- assingInstructorsRequest: (state) => {
+ assignInstructorsRequest: (state) => {
state.assignInstructors.status = RequestStatus.LOADING;
},
- assingInstructorsSuccess: (state, { payload }) => {
+ assignInstructorsSuccess: (state, { payload }) => {
state.assignInstructors.status = RequestStatus.SUCCESS;
state.assignInstructors.data = payload;
},
- assingInstructorsFailed: (state) => {
+ assignInstructorsFailed: (state) => {
state.assignInstructors.status = RequestStatus.ERROR;
},
addRowSelect: (state, { payload }) => {
@@ -108,9 +108,9 @@ export const {
fetchCoursesDataFailed,
updateFilters,
updateClassSelected,
- assingInstructorsRequest,
- assingInstructorsSuccess,
- assingInstructorsFailed,
+ assignInstructorsRequest,
+ assignInstructorsSuccess,
+ assignInstructorsFailed,
addRowSelect,
deleteRowSelect,
resetRowSelect,
diff --git a/src/features/Instructors/data/thunks.js b/src/features/Instructors/data/thunks.js
index ab9f8e82..389d246d 100644
--- a/src/features/Instructors/data/thunks.js
+++ b/src/features/Instructors/data/thunks.js
@@ -9,15 +9,13 @@ import {
fetchCoursesDataRequest,
fetchCoursesDataSuccess,
fetchCoursesDataFailed,
- assingInstructorsRequest,
- assingInstructorsSuccess,
- assingInstructorsFailed,
+ assignInstructorsRequest,
+ assignInstructorsSuccess,
+ assignInstructorsFailed,
addInstructorRequest,
addInstructorSuccess,
addInstructorFailed,
- resetRowSelect,
} from 'features/Instructors/data/slice';
-import { fetchClassesData as fetchClassesDataHome } from 'features/Dashboard/data';
import { initialPage } from 'features/constants';
function fetchInstructorsData(id, currentPage, filtersData) {
@@ -50,21 +48,17 @@ function fetchCoursesData(id) {
* Assign instructors to a class.
* @param {Object} data - The data containing information about the instructors to be assigned.
* @param {string} classId - The ID of the class to which the instructors will be assigned.
- * @param {number} institutionId - The ID of the institution associated with the class.
* @returns {Promise} - A promise that resolves after dispatching appropriate actions.
*/
-function assignInstructors(data, classId, institutionId) {
+function assignInstructors(data, classId) {
return async (dispatch) => {
- dispatch(assingInstructorsRequest());
+ dispatch(assignInstructorsRequest());
try {
const response = await handleInstructorsEnrollment(data, classId);
- dispatch(assingInstructorsSuccess(response.data));
+ dispatch(assignInstructorsSuccess(response.data));
} catch (error) {
- dispatch(assingInstructorsFailed());
+ dispatch(assignInstructorsFailed());
logError(error);
- } finally {
- dispatch(fetchClassesDataHome(institutionId, false));
- dispatch(resetRowSelect());
}
};
}
diff --git a/src/features/Main/index.jsx b/src/features/Main/index.jsx
index b7e6e994..86d9f34d 100644
--- a/src/features/Main/index.jsx
+++ b/src/features/Main/index.jsx
@@ -16,6 +16,7 @@ import DashboardPage from 'features/Dashboard/DashboardPage';
import LicensesPage from 'features/Licenses/LicensesPage';
import ClassesPage from 'features/Classes/ClassesPage';
import { Select } from 'react-paragon-topaz';
+import CoursesDetailPage from 'features/Courses/CoursesDetailPage';
import { fetchInstitutionData } from 'features/Main/data/thunks';
import { updateSelectedInstitution } from 'features/Main/data/slice';
@@ -91,6 +92,9 @@ const Main = () => {
+
+
+
diff --git a/src/features/Students/StudentsMetrics/index.scss b/src/features/Students/StudentsMetrics/index.scss
index e1789063..d0b0dba6 100644
--- a/src/features/Students/StudentsMetrics/index.scss
+++ b/src/features/Students/StudentsMetrics/index.scss
@@ -1,9 +1,10 @@
@import "assets/colors.scss";
+@import "assets/variables.scss";
.container-cards {
border: 1px solid $gray-20;
padding: 2rem;
- border-radius: 0.375rem;
+ border-radius: $border-radius-1;
margin-bottom: 2rem;
box-shadow: 0px 3px 12px 0px rgba(0, 0, 0, 0.16);
background-color: $color-white;
diff --git a/src/features/constants.js b/src/features/constants.js
index d88a9759..3353f177 100644
--- a/src/features/constants.js
+++ b/src/features/constants.js
@@ -27,7 +27,7 @@ export const initialStateService = {
export const initialPage = 1;
/**
- * Constans for time.
+ * Constants for time.
* @readonly
* @number
*/