Skip to content

Commit

Permalink
feat: Classes table in courses view
Browse files Browse the repository at this point in the history
  • Loading branch information
01001110J committed Feb 26, 2024
1 parent 2e94efc commit 973fda4
Show file tree
Hide file tree
Showing 19 changed files with 646 additions and 24 deletions.
26 changes: 17 additions & 9 deletions src/assets/colors.scss
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
// Color variables
// Primary
$primary: #007394;
$primary-dark: #003057;

// General
$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;
9 changes: 7 additions & 2 deletions src/assets/global.scss
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}

Expand All @@ -34,3 +35,7 @@
list-style-type: none;
padding-left: 0;
}

.link {
color: $primary !important;
}
3 changes: 3 additions & 0 deletions src/assets/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@

$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;
4 changes: 2 additions & 2 deletions src/features/Classes/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
148 changes: 148 additions & 0 deletions src/features/Courses/CourseDetailTable/__test__/columns.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { Provider } from 'react-redux';
import { initializeStore } from 'store';
import {
fireEvent,
render,
waitFor,
} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { MemoryRouter, Route } from 'react-router-dom';

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, ' / ', 4] });
});

test('Should render the instructors', () => {
const values = {
row: {
values: { instructors: ['Sam Sepiol'] },
original: {
classId: 'Demo Course 1',
},
},
};

const Component = () => columns[1].Cell(values);

const store = initializeStore();
const { container } = render(
<Provider store={store}>
<MemoryRouter initialEntries={['/courses/Demo%20Course%201']}>
<Route path="/courses/:classId">
<Component />
</Route>
</MemoryRouter>
</Provider>,
);

expect(container.querySelector('li').textContent).toBe('Sam Sepiol');
});

test('Should render the assign button if instructor is not present', async () => {
const store = initializeStore();
const values = {
row: {
values: { instructors: [] },
original: {
classId: 'Demo Course 1',
},
},
};

const ComponentNoInstructor = () => columns[1].Cell(values);
const { container, getAllByText } = render(
<Provider store={store}>
<MemoryRouter initialEntries={['/courses/Demo%20Course%201']}>
<Route path="/courses/:classId">
<ComponentNoInstructor />
</Route>
</MemoryRouter>
</Provider>,
);

const modalButton = container.querySelector('button');
expect(modalButton).toBeInTheDocument();

fireEvent.click(modalButton);

await waitFor(() => {
const title = getAllByText('Assign instructor')[0];
expect(title).toBeInTheDocument();
});
});
});
69 changes: 69 additions & 0 deletions src/features/Courses/CourseDetailTable/__test__/index.test.jsx
Original file line number Diff line number Diff line change
@@ -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(<CourseDetailTable data={[]} count={0} columns={[]} />);
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(
<MemoryRouter initialEntries={['/courses']}>
<Route path="/courses">
<CourseDetailTable
data={mockStore.classes.table.data}
count={mockStore.classes.table.data.length}
columns={columns}
/>
</Route>
</MemoryRouter>,
{ 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');
});
});
90 changes: 90 additions & 0 deletions src/features/Courses/CourseDetailTable/columns.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable react/prop-types */
import React, { useState } from 'react';
import { Badge, Button } from 'react-paragon-topaz';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { format } from 'date-fns';

import { fetchClassesData } from 'features/Classes/data/thunks';
import { updateClassSelected } from 'features/Instructors/data/slice';
import AssignInstructors from 'features/Instructors/AssignInstructors';

const columns = [
{
Header: 'Class',
accessor: 'className',
Cell: ({ row }) => (<span className="text-truncate">{row.values.className}</span>),
},
{
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, 1, classId));
setIsModalOpen(false);
};

if (row.values.instructors?.length > 0) {
return (
<ul className="instructors-list mb-0">
{row.values.instructors.map(instructor => <li key={instructor} className="text-truncate">{`${instructor}`}</li>)}
</ul>
);
}

return (
<>
<Button onClick={handleOpenModal} className="button">
<i className="fa fa-plus mr-2" />
Assign
</Button>
<AssignInstructors
isOpen={isModalOpen}
close={handleCloseModal}
/>
</>
);
},
},
{
Header: 'Enrollment status',
accessor: 'numberOfPendingStudents',
Cell: ({ row }) => {
const isEnrollmentComplete = row.values.numberOfPendingStudents === 0;

return isEnrollmentComplete ? <Badge variant="success" light>Complete</Badge> : <Badge variant="warning" light>Pending &#40;{row.values.numberOfPendingStudents}&#41;</Badge>;
},
},
{
Header: 'Students Enrolled',
accessor: 'numberOfStudents',
Cell: ({ row }) => (<span>{row.values.numberOfStudents}{' / '}{row.values.numberOfStudents + row.values.numberOfPendingStudents}</span>),
},
{
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 };
Loading

0 comments on commit 973fda4

Please sign in to comment.