Skip to content

Commit

Permalink
feat: Add assign instructor modal
Browse files Browse the repository at this point in the history
  • Loading branch information
AuraAlba committed Feb 12, 2024
1 parent 0a7c302 commit 7aca9b0
Show file tree
Hide file tree
Showing 13 changed files with 495 additions and 31 deletions.
37 changes: 28 additions & 9 deletions src/features/Dashboard/InstructorAssignSection/ClassCard.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { format } from 'date-fns';
import PropTypes from 'prop-types';

import { Button } from 'react-paragon-topaz';
import { useToggle } from '@edx/paragon';
import AssignInstructors from 'features/Instructors/AssignInstructors';

import { updateClassSelected } from 'features/Instructors/data/slice';

import 'features/Dashboard/InstructorAssignSection/index.scss';

const ClassCard = ({ data }) => {
const dispatch = useDispatch();
const [isOpen, open, close] = useToggle(false);
const fullDate = format(new Date(data.startDate), 'PP');

const handleAssignModal = () => {
dispatch(updateClassSelected(data.classId)); // eslint-disable-line react/prop-types
open();
};

return (
<div className="class-card-container">
<h4>{data?.className}</h4>
<p className="course-name">{data?.masterCourseName}</p>
<p className="date"><i className="fa-sharp fa-regular fa-calendar-day" />{fullDate}</p>
<Button variant="outline-primary" size="sm">
<i className="fa-regular fa-chalkboard-user" />
Assign instructor
</Button>
</div>
<>
<AssignInstructors
isOpen={isOpen}
close={close}
/>
<div className="class-card-container">
<h4>{data?.className}</h4>
<p className="course-name">{data?.masterCourseName}</p>
<p className="date"><i className="fa-sharp fa-regular fa-calendar-day" />{fullDate}</p>
<Button variant="outline-primary" size="sm" onClick={handleAssignModal}>
<i className="fa-regular fa-chalkboard-user" />
Assign instructor
</Button>
</div>
</>

);
};

Expand Down
3 changes: 3 additions & 0 deletions src/features/Dashboard/InstructorAssignSection/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const InstructorAssignSection = () => {
<Button text className="view-all-btn">View all</Button>
</div>
)}
{classesData.length === 0 && (
<div className="empty-content">No classes found</div>
)}
</Col>
</Row>
);
Expand Down
5 changes: 5 additions & 0 deletions src/features/Dashboard/InstructorAssignSection/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
}
}

.instructor-assign-section .empty-content {
padding: 1rem;
font-size: 1rem;
}

.class-card-container {
padding: 1rem;
border-bottom: 1px solid $gray-20;
Expand Down
45 changes: 45 additions & 0 deletions src/features/Instructors/AssignInstructors/AssignTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useMemo, useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';

import { IntlProvider } from 'react-intl';
import {
Row,
Col,
DataTable,
} from '@edx/paragon';

import { updateRowsSelected } from 'features/Instructors/data/slice';

import { columns } from 'features/Instructors/AssignInstructors/columns';

const AssignTable = () => {
const dispatch = useDispatch();
const stateInstructors = useSelector((state) => state.instructors.table);
const [rowsSelected, setRowsSelected] = useState([]);
const COLUMNS = useMemo(() => columns({ setRowsSelected }), []);

useEffect(() => {
dispatch(updateRowsSelected(rowsSelected));
}, [rowsSelected, dispatch]);

return (
<IntlProvider locale="en">
<Row className="justify-content-center my-4 my-3">
<Col xs={11} className="p-0">
<DataTable
isSortable
columns={COLUMNS}
itemCount={stateInstructors.count}
data={stateInstructors.data}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content="No instructors found." />
</DataTable>
</Col>
</Row>
</IntlProvider>
);
};

export default AssignTable;
64 changes: 64 additions & 0 deletions src/features/Instructors/AssignInstructors/_test_/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { waitFor, fireEvent } from '@testing-library/react';
import AssignInstructors from 'features/Instructors/AssignInstructors';
import '@testing-library/jest-dom/extend-expect';
import { renderWithProviders } from 'test-utils';

jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));

const mockStore = {
instructors: {
table: {
data: [
{
instructorUsername: 'Instructor1',
instructorName: 'Instructor 1',
instructorEmail: '[email protected]',
ccxId: 'CCX1',
ccxName: 'CCX 1',
},
{
instructorUsername: 'Instructor2',
instructorName: 'Instructor 2',
instructorEmail: '[email protected]',
ccxId: 'CCX2',
ccxName: 'CCX 2',
},
],
count: 2,
num_pages: 1,
current_page: 1,
},
classes: {
data: [],
},
courses: {
data: [],
},
},
};

describe('Assign instructors modal', () => {
test('render assing instructors modal', () => {
const {getByText, getAllByRole, getByTestId} = renderWithProviders(

Check failure on line 45 in src/features/Instructors/AssignInstructors/_test_/index.test.jsx

View workflow job for this annotation

GitHub Actions / test

A space is required after '{'

Check failure on line 45 in src/features/Instructors/AssignInstructors/_test_/index.test.jsx

View workflow job for this annotation

GitHub Actions / test

A space is required before '}'
<AssignInstructors isOpen close={() => {}} />,
{ preloadedState: mockStore },
);

waitFor(() => {
expect(getByText('Instructor')).toBeInTheDocument();
expect(getByText('Instructor1')).toBeInTheDocument()

Check failure on line 52 in src/features/Instructors/AssignInstructors/_test_/index.test.jsx

View workflow job for this annotation

GitHub Actions / test

Missing semicolon
expect(getByText('Instructor2')).toBeInTheDocument()

Check failure on line 53 in src/features/Instructors/AssignInstructors/_test_/index.test.jsx

View workflow job for this annotation

GitHub Actions / test

Missing semicolon
expect(getByText('Last seen')).toBeInTheDocument()

Check failure on line 54 in src/features/Instructors/AssignInstructors/_test_/index.test.jsx

View workflow job for this annotation

GitHub Actions / test

Missing semicolon
expect(getByText('Courses taught')).toBeInTheDocument()

Check failure on line 55 in src/features/Instructors/AssignInstructors/_test_/index.test.jsx

View workflow job for this annotation

GitHub Actions / test

Missing semicolon
});

const checkboxFields = getAllByRole('checkbox');
fireEvent.click(checkboxFields[0])

Check failure on line 59 in src/features/Instructors/AssignInstructors/_test_/index.test.jsx

View workflow job for this annotation

GitHub Actions / test

Missing semicolon

const assignButton = getByTestId('assignButton')

Check failure on line 61 in src/features/Instructors/AssignInstructors/_test_/index.test.jsx

View workflow job for this annotation

GitHub Actions / test

Missing semicolon
fireEvent.click(assignButton)

Check failure on line 62 in src/features/Instructors/AssignInstructors/_test_/index.test.jsx

View workflow job for this annotation

GitHub Actions / test

Missing semicolon
});
});
58 changes: 58 additions & 0 deletions src/features/Instructors/AssignInstructors/columns.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* eslint-disable react/prop-types, no-nested-ternary */
import { differenceInHours, differenceInDays, differenceInWeeks } from 'date-fns';
import { CheckboxControl } from '@edx/paragon';

const handleCheckbox = (setRowsSelected, row) => {
setRowsSelected(prevState => {
const rowSelected = row.original.instructorUsername;
if (prevState.includes(rowSelected)) {
const filterData = prevState.filter(rowState => rowState !== rowSelected);
return filterData;
}
return [...prevState, rowSelected];
});
};

const columns = ({ setRowsSelected }) => [
{
Header: '',
id: 'checkbox',
width: 30,
Cell: ({ row }) => (
<div>
<CheckboxControl
onChange={() => handleCheckbox(setRowsSelected, row)}
/>
</div>
),
},
{
Header: 'Instructor',
accessor: 'instructorName',
},
{
Header: 'Last seen',
accessor: 'lastAccess',
Cell: ({ row }) => {
const currentDate = Date.now();
const lastDate = new Date(row.values.lastAccess);
const diffHours = differenceInHours(currentDate, lastDate);
const diffDays = differenceInDays(currentDate, lastDate);
const diffWeeks = differenceInWeeks(currentDate, lastDate);
return (
<span>{diffHours < 24
? 'Today'
: diffDays < 7
? `${diffDays} days ago`
: `${diffWeeks} wks ago`}
</span>
);
},
},
{
Header: 'Courses Taught',
accessor: 'classes',
},
];

export { columns };
102 changes: 102 additions & 0 deletions src/features/Instructors/AssignInstructors/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';

import { ModalDialog, ModalCloseButton, Pagination } from '@edx/paragon';
import { Button } from 'react-paragon-topaz';
import InstructorsFilters from 'features/Instructors/InstructorsFilters';
import AssignTable from 'features/Instructors/AssignInstructors/AssignTable';

import { fetchInstructorsData, assignInstructors } from 'features/Instructors/data';
import {
updateCurrentPage, updateRowsSelected, updateFilters, updateClassSelected,
} from 'features/Instructors/data/slice';

import { initialPage } from 'features/constants';
import 'features/Instructors/AssignInstructors/index.scss';

const AssignInstructors = ({ isOpen, close }) => {
const dispatch = useDispatch();
const selectedInstitution = useSelector((state) => state.main.selectedInstitution);
const stateInstructors = useSelector((state) => state.instructors);
const rowsSelected = useSelector((state) => state.instructors.rowsSelected);
const classId = useSelector((state) => state.instructors.classSelected);
const [currentPage, setCurrentPage] = useState(initialPage);

const resetPagination = () => {
setCurrentPage(initialPage);
};

const handlePagination = (targetPage) => {
setCurrentPage(targetPage);
dispatch(updateCurrentPage(targetPage));
};

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

useEffect(() => {
if (Object.keys(selectedInstitution).length > 0) {
dispatch(fetchInstructorsData(selectedInstitution.id, currentPage, stateInstructors.filters));
}
}, [currentPage, selectedInstitution, dispatch]); // eslint-disable-line react-hooks/exhaustive-deps

useEffect(() => {
if (!isOpen) {
dispatch(updateRowsSelected([]));
dispatch(updateFilters({}));
dispatch(updateClassSelected(''));
}
}, [isOpen, dispatch]);

return (
<ModalDialog
title="Assign instructor"
isOpen={isOpen}
onClose={close}
hasCloseButton
size="lg"
>
<ModalDialog.Header>
<ModalDialog.Title>
Assign instructor
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<InstructorsFilters isAssignModal resetPagination={resetPagination} />
<AssignTable />
{stateInstructors.table.numPages > 1 && (
<Pagination
paginationLabel="paginationNavigation"
pageCount={stateInstructors.table.numPages}
currentPage={currentPage}
onPageSelect={handlePagination}
variant="reduced"
className="mx-auto pagination-table"
size="small"
/>
)}
<div className="d-flex justify-content-end">
<ModalCloseButton className="btntpz btn-text btn-tertiary">Close</ModalCloseButton>
<Button onClick={handleAssignInstructors} data-testid='assignButton'>Assign instructor</Button>

Check failure on line 90 in src/features/Instructors/AssignInstructors/index.jsx

View workflow job for this annotation

GitHub Actions / test

Unexpected usage of singlequote
</div>
</ModalDialog.Body>
</ModalDialog>
);
};

AssignInstructors.propTypes = {
isOpen: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
};

export default AssignInstructors;
13 changes: 13 additions & 0 deletions src/features/Instructors/AssignInstructors/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@import "assets/colors.scss";

.pgn__modal-close-button.btn-icon.btn-icon-primary {
color: $gray-70;

&:hover,
&:active,
&:focus {
color: $gray-70;
background-color: $gray-20;
box-shadow: none;
}
}
Loading

0 comments on commit 7aca9b0

Please sign in to comment.