Skip to content

Commit

Permalink
Merge pull request #35 from Pearson-Advance/vue/PADV-966
Browse files Browse the repository at this point in the history
PADV-966 feat: Add assign instructors modal
  • Loading branch information
AuraAlba authored Feb 15, 2024
2 parents 3a08a36 + a6c0b4f commit fd6acf8
Show file tree
Hide file tree
Showing 16 changed files with 567 additions and 33 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
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

0 comments on commit fd6acf8

Please sign in to comment.