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-966 feat: Add assign instructors modal #35

Merged
merged 2 commits into from
Feb 15, 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
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
50 changes: 50 additions & 0 deletions src/features/Instructors/AssignInstructors/AssignTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';

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

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

const AssignTable = () => {
const stateInstructors = useSelector((state) => state.instructors.table);
const COLUMNS = useMemo(() => columns(), []);

const selectColumn = {
id: 'selection',
Header: <></>, // eslint-disable-line react/jsx-no-useless-fragment
Cell: ControlledSelect,
disableSortBy: true,
};

return (
<IntlProvider locale="en">
<Row className="justify-content-center my-4 my-3">
<Col xs={11} className="p-0">
<DataTable
isSortable
isSelectable
columns={COLUMNS}
itemCount={stateInstructors.count}
data={stateInstructors.data}
manualSelectColumn={selectColumn}
initialTableOptions={{
autoResetSelectedRows: false,
getRowId: (row) => row.instructorUsername,
}}
>
<DataTable.Table />
<DataTable.EmptyTable content="No instructors found." />
</DataTable>
</Col>
</Row>
</IntlProvider>
);
};

export default AssignTable;
58 changes: 58 additions & 0 deletions src/features/Instructors/AssignInstructors/ControlledSelect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, {
useCallback, useMemo,
} from 'react';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';

import { CheckboxControl } from '@edx/paragon';
import { addRowSelect, deleteRowSelect } from 'features/Instructors/data/slice';

const useConvertIndeterminateProp = (props) => {
const updatedProps = useMemo(
() => {
const { indeterminate, ...rest } = props;
return { isIndeterminate: indeterminate, ...rest };
},
[props],
);
return updatedProps;
};

const ControlledSelect = ({ row }) => {
const dispatch = useDispatch();

const toggleSelected = useCallback(
() => {
if (row.isSelected) {
dispatch(deleteRowSelect(row.id));
row.toggleRowSelected();
} else {
dispatch(addRowSelect(row.id));
row.toggleRowSelected();
}
},
[row, dispatch],
);

const updatedProps = useConvertIndeterminateProp(row.getToggleRowSelectedProps());

return (
<div className="test-checkbox">
<CheckboxControl
{...updatedProps}
onChange={toggleSelected}
/>
</div>
);
};

ControlledSelect.propTypes = {
row: PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
getToggleRowSelectedProps: PropTypes.func.isRequired,
isSelected: PropTypes.bool.isRequired,
toggleRowSelected: PropTypes.func.isRequired,
}).isRequired,
};

export default ControlledSelect;
74 changes: 74 additions & 0 deletions src/features/Instructors/AssignInstructors/_test_/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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';
import { RequestStatus } from 'features/constants';

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: [],
},
filters: {
},
rowsSelected: [],
classSelected: '',
assignInstructors: {
status: RequestStatus.LOADING,
error: null,
data: [],
},
},
};

describe('Assign instructors modal', () => {
test('render assing instructors modal', () => {
const { getByText, getAllByRole, getByTestId } = renderWithProviders(
<AssignInstructors isOpen close={() => {}} />,
{ preloadedState: mockStore },
);

waitFor(() => {
expect(getByText('Instructor')).toBeInTheDocument();
expect(getByText('Instructor1')).toBeInTheDocument();
expect(getByText('Instructor2')).toBeInTheDocument();
expect(getByText('Last seen')).toBeInTheDocument();
expect(getByText('Courses taught')).toBeInTheDocument();
});

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

const assignButton = getByTestId('assignButton');
fireEvent.click(assignButton);
});
});
37 changes: 37 additions & 0 deletions src/features/Instructors/AssignInstructors/columns.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable react/prop-types, no-nested-ternary */
import { differenceInHours, differenceInDays, differenceInWeeks } from 'date-fns';

import { daysWeek, hoursDay } from 'features/constants';

const columns = () => [
{
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 < hoursDay
? 'Today'
: diffDays < daysWeek
? `${diffDays} days ago`
: `${diffWeeks} wks ago`}
</span>
);
},
},
{
Header: 'Courses Taught',
accessor: 'classes',
disableSortBy: true,
},
];

export { columns };
101 changes: 101 additions & 0 deletions src/features/Instructors/AssignInstructors/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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, 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(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">Cancel</ModalCloseButton>
<Button onClick={handleAssignInstructors} data-testid="assignButton">Assign instructor</Button>
</div>
</ModalDialog.Body>
</ModalDialog>
);
};

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

export default AssignInstructors;
Loading
Loading