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 ( + + ); + } + + 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 */