diff --git a/README.md b/README.md index b87cb004..e95163fe 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ This project was bootstrapped with [Create React App](https://github.com/faceboo In the project directory, you can run: +### `npm install` +Installs the dependencies required + ### `npm start` Runs the app in the development mode.\ diff --git a/package-lock.json b/package-lock.json index fe94f13f..c520552b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5831,4 +5831,4 @@ } } } -} +} \ No newline at end of file diff --git a/public/assets/icons/Check-icon.png b/public/assets/icons/Check-icon.png new file mode 100644 index 00000000..c4d5504e Binary files /dev/null and b/public/assets/icons/Check-icon.png differ diff --git a/src/components/ColumnButton.test.tsx b/src/components/ColumnButton.test.tsx new file mode 100644 index 00000000..20e22b42 --- /dev/null +++ b/src/components/ColumnButton.test.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import ColumnButton from "./ColumnButton"; + +// Mock React-Bootstrap components +jest.mock("react-bootstrap", () => ({ + Button: ({ children, ...props }: any) => , + Tooltip: ({ id, children }: any) =>
{children}
, + OverlayTrigger: ({ children, overlay }: any) => ( +
+ {overlay} + {children} +
+ ), +})); + +describe("ColumnButton", () => { + const mockOnClick = jest.fn(); + + const baseProps = { + id: "test-button", + label: "Test Button", + tooltip: "This is a test tooltip", + variant: "primary", + size: "sm" as const, + className: "custom-class", + onClick: mockOnClick, + icon: Icon, + }; + + test("should call onClick when the button is clicked", async () => { + render(); + const button = screen.getByRole("button", { name: "Test Button" }); + + userEvent.click(button); + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); + + test("should render a tooltip when tooltip is provided", () => { + render(); + const tooltip = screen.getByText("This is a test tooltip"); + + // Vanilla assertion to check if tooltip is rendered + expect(tooltip).not.toBeNull(); + const button = screen.getByRole("button", { name: "Test Button" }); + expect(button).not.toBeNull(); // Ensure the button is still present + }); + + test("should not render a tooltip when tooltip is not provided", () => { + render(); + const tooltip = screen.queryByText("This is a test tooltip"); + + // Vanilla assertion to check if tooltip is not rendered + expect(tooltip).toBeNull(); + }); +}); diff --git a/src/components/ColumnButton.tsx b/src/components/ColumnButton.tsx new file mode 100644 index 00000000..25eea86f --- /dev/null +++ b/src/components/ColumnButton.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { Button, OverlayTrigger, Tooltip } from "react-bootstrap"; + +/** + * @author Rutvik Kulkarni on Nov, 2024 + */ + +interface ColumnButtonProps { + id: string; + label?: string; + tooltip?: string; + variant: string; + size?: "sm" | "lg"; // Matches React-Bootstrap Button prop + className?: string; + onClick: () => void; + icon: React.ReactNode; + } + + const ColumnButton: React.FC = (props) => { + const { + id, + label, + tooltip, + variant, + size, + className, + onClick, + icon, + } = props; + + const displayButton = ( + + ); + + if (tooltip) { + return ( + {tooltip}} + > + {displayButton} + + ); + } + + return displayButton; + }; + + export default ColumnButton; \ No newline at end of file diff --git a/src/components/Form/FormCheckBoxGroup.tsx b/src/components/Form/FormCheckBoxGroup.tsx index 0534d0eb..311e6855 100644 --- a/src/components/Form/FormCheckBoxGroup.tsx +++ b/src/components/Form/FormCheckBoxGroup.tsx @@ -30,6 +30,7 @@ const FormCheckboxGroup: React.FC = (props) => { void; getPageCount: () => number; getState: () => TableState; + totalItems: number; } const Pagination: React.FC = (props) => { @@ -29,7 +30,12 @@ const Pagination: React.FC = (props) => { setPageSize, getPageCount, getState, + totalItems, } = props; + const pageSize = getState().pagination.pageSize; + if (totalItems <= pageSize) { + return null; + } return ( @@ -69,6 +75,7 @@ const Pagination: React.FC = (props) => { { label: "Show 10", value: "10" }, { label: "Show 25", value: "25" }, { label: "Show 50", value: "50" }, + { label: "Show All", value: String(totalItems) }, ]} input={{ value: getState().pagination.pageSize, diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index a9e5391b..244de344 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -18,7 +18,6 @@ import { import GlobalFilter from "./GlobalFilter"; import Pagination from "./Pagination"; import RowSelectCheckBox from "./RowSelectCheckBox"; - import { FaSearch } from "react-icons/fa"; interface TableProps { data: Record[]; @@ -78,6 +77,7 @@ import { ); }, + size: 40, enableSorting: false, enableColumnFilter: false, }; @@ -103,6 +103,7 @@ import { }} /> ), + size: 40, enableSorting: false, enableFilter: false, }] : []; @@ -134,6 +135,11 @@ import { getPaginationRowModel: getPaginationRowModel(), getExpandedRowModel: getExpandedRowModel(), }); + + //Enable search filters for columns based on page size + const totalItems = initialData.length; + const pageSize = table.getState().pagination.pageSize; + const shouldShowColumnFilters = showColumnFilter && totalItems > pageSize; const flatRows = table.getSelectedRowModel().flatRows; @@ -166,21 +172,21 @@ import { )} - - - {isGlobalFilterVisible ? " Hide" : " Show"} - + {/* + + {isGlobalFilterVisible ? " Hide" : " Show"} + */} - + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( - + {header.isPlaceholder ? null : ( <>
- {showColumnFilter && header.column.getCanFilter() ? ( + {shouldShowColumnFilters && header.column.getCanFilter() ? ( ) : null} @@ -224,6 +230,7 @@ import { )} + ))} @@ -238,6 +245,7 @@ import { setPageSize={table.setPageSize} getPageCount={table.getPageCount} getState={table.getState} + totalItems={initialData.length} /> )} diff --git a/src/index.css b/src/index.css index 69cf840e..c014894e 100644 --- a/src/index.css +++ b/src/index.css @@ -5,4 +5,64 @@ html { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; +} + +.custom-table-layout { + /* This forces the table to use a fixed layout algorithm. */ + /* Column widths are determined by the table's width, not content. */ + table-layout: fixed; + width: 100%; +} + +.custom-table-layout th, +.custom-table-layout td { + /* This adds consistent, reasonable spacing inside all headers and cells. */ + padding: 0.75rem 1rem; + + /* This allows long text to wrap, preventing it from stretching the column. */ + word-wrap: break-word; + + /* This vertically aligns content in the middle, which looks cleaner. */ + vertical-align: middle; +} + +/* Make checkbox group readable on dark or busy backgrounds */ +/* .checkbox-contrast .form-check { + background-color: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.5rem; + padding: 0.5rem 0.75rem; + margin-bottom: 0.5rem; +} */ + +/* Bigger, clearer boxes */ +.checkbox-contrast .form-check-input { + width: 1rem; + height: 1rem; + border: 1px solid black; /* Bootstrap primary */ + box-shadow: none; + margin-top: 0.25rem; /* aligns with label text */ +} + +/* Solid “checked” state */ +.checkbox-contrast .form-check-input:checked { + background-color: #0d6efd; + border-color: #0d6efd; +} + +/* Make labels bold & darker for readability */ +/* .checkbox-contrast .form-check-label { + font-weight: 700; + color: #0b1324; +} */ + +/* Optional: clearer focus outline for keyboard users */ +/* .checkbox-contrast .form-check-input:focus { + outline: none; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} */ + +.custom-table-layout { + table-layout: auto; + width: 100%; } \ No newline at end of file diff --git a/src/pages/Authentication/Login.tsx b/src/pages/Authentication/Login.tsx index 2051b297..c17430b0 100644 --- a/src/pages/Authentication/Login.tsx +++ b/src/pages/Authentication/Login.tsx @@ -33,7 +33,7 @@ const Login: React.FC = () => { .post("http://localhost:3002/login", values) .then((response) => { const payload = setAuthToken(response.data.token); - + localStorage.setItem("session", JSON.stringify({ user: payload })); dispatch( authenticationActions.setAuthentication({ authToken: response.data.token, diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index 599e22b0..27e9b067 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -1,12 +1,12 @@ import { Row as TRow } from "@tanstack/react-table"; -import Table from "../../components/Table/Table"; -import useAPI from "../../hooks/useAPI"; +import Table from "components/Table/Table"; +import useAPI from "hooks/useAPI"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Button, Col, Container, Row } from "react-bootstrap"; import { RiHealthBookLine } from "react-icons/ri"; import { useDispatch, useSelector } from "react-redux"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; -import { alertActions } from "../../store/slices/alertSlice"; +import { alertActions } from "store/slices/alertSlice"; import { RootState } from "../../store/store"; import { ICourseResponse, ROLE } from "../../utils/interfaces"; import { courseColumns as COURSE_COLUMNS } from "./CourseColumns"; @@ -24,10 +24,12 @@ const Courses = () => { const { error, isLoading, data: CourseResponse, sendRequest: fetchCourses } = useAPI(); const { data: InstitutionResponse, sendRequest: fetchInstitutions } = useAPI(); const { data: InstructorResponse, sendRequest: fetchInstructors } = useAPI(); + const { data: assignmentResponse, sendRequest: fetchAssignments } = useAPI(); const auth = useSelector( (state: RootState) => state.authentication, (prev, next) => prev.isAuthenticated === next.isAuthenticated ); + const currUserRole = auth.user.role.valueOf(); const navigate = useNavigate(); const location = useLocation(); const dispatch = useDispatch(); @@ -49,11 +51,13 @@ const Courses = () => { fetchCourses({ url: `/courses` }); fetchInstitutions({ url: `/institutions` }); fetchInstructors({ url: `/users` }); + fetchAssignments({ url: `/assignments` }); } }, [ fetchCourses, fetchInstitutions, fetchInstructors, + fetchAssignments, location, showDeleteConfirmation.visible, auth.user.id, @@ -108,8 +112,7 @@ const renderSubComponent = useCallback(({ row }: { row: TRow }) }, []); const tableColumns = useMemo( - () => - COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle), + () => COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle), [onDeleteHandle, onEditHandle, onTAHandle, onCopyHandle] ); @@ -155,6 +158,11 @@ const renderSubComponent = useCallback(({ row }: { row: TRow }) ); }, [mergedTableData, loggedInUserRole]); + const coursesWithAssignments = useMemo(() => { + if (!assignmentResponse?.data) return new Set(); + return new Set(assignmentResponse.data.map((a: any) => a.course_id)); + }, [assignmentResponse?.data]); + return ( <> @@ -204,11 +212,16 @@ const renderSubComponent = useCallback(({ row }: { row: TRow }) columns={tableColumns} columnVisibility={{ id: false, - institution: auth.user.role === ROLE.SUPER_ADMIN.valueOf(), - instructor: auth.user.role === ROLE.SUPER_ADMIN.valueOf(), + institution: + auth.user.role === ROLE.SUPER_ADMIN.valueOf() || + auth.user.role === ROLE.ADMIN.valueOf(), + instructor: + auth.user.role === ROLE.SUPER_ADMIN.valueOf() || + auth.user.role === ROLE.ADMIN.valueOf(), }} renderSubComponent={renderSubComponent} getRowCanExpand={() => true} + //getRowCanExpand={(row: TRow) => coursesWithAssignments.has(row.original.id)} /> diff --git a/src/pages/Courses/CourseAssignments.tsx b/src/pages/Courses/CourseAssignments.tsx index a8f0ae22..a22c46c2 100644 --- a/src/pages/Courses/CourseAssignments.tsx +++ b/src/pages/Courses/CourseAssignments.tsx @@ -1,7 +1,7 @@ import { Row as TRow } from "@tanstack/react-table"; -import Table from "../../components/Table/Table"; +import Table from "components/Table/Table"; import React from 'react'; -import useAPI from "../../hooks/useAPI"; +import useAPI from "hooks/useAPI"; import { useCallback, useEffect, useState } from "react"; import { assignmentColumns as getBaseAssignmentColumns } from "../Assignments/AssignmentColumns"; @@ -146,8 +146,8 @@ const CourseAssignments: React.FC = ({ courseId, courseN const columns = getAssignmentColumns(actionHandlers); return ( -
-
Assignments for {courseName}
+
+ {/*
Assignments for {courseName}
*/} ({ id, ...props }: any) => ( + +)); + +describe("courseColumns", () => { + const mockHandleEdit = jest.fn(); + const mockHandleDelete = jest.fn(); + const mockHandleTA = jest.fn(); + const mockHandleCopy = jest.fn(); + const mockRow: Partial> = { + original: { id: "123", name: "Test Course", institution: { name: "Test Institution" } }, + }; + + test("should define all required columns", () => { + const columns = courseColumns(mockHandleEdit, mockHandleDelete, mockHandleTA, mockHandleCopy); + expect(columns).toHaveLength(5); + + // Check each column's header + expect(columns[0].header).toBe("Name"); + expect(columns[1].header).toBe("Institution"); + expect(columns[2].header).toBe("Creation Date"); + expect(columns[3].header).toBe("Updated Date"); + expect(columns[4].header).toBe("Actions"); + }); + + test("should call handleEdit when edit button is clicked", async () => { + const actionsColumn = courseColumns( + mockHandleEdit, + mockHandleDelete, + mockHandleTA, + mockHandleCopy, + //"Super Administrator" + ).find((col) => col.id === "actions"); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const editButton = screen.getByTestId("edit"); + + userEvent.click(editButton); + expect(mockHandleEdit).toHaveBeenCalledTimes(1); + expect(mockHandleEdit).toHaveBeenCalledWith(mockRow); + }); + + test("should call handleDelete when delete button is clicked", async () => { + const actionsColumn = courseColumns( + mockHandleEdit, + mockHandleDelete, + mockHandleTA, + mockHandleCopy, + //"Super Administrator" + ).find((col) => col.id === "actions"); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const deleteButton = screen.getByTestId("delete"); + + userEvent.click(deleteButton); + expect(mockHandleDelete).toHaveBeenCalledTimes(1); + expect(mockHandleDelete).toHaveBeenCalledWith(mockRow); + }); + + test("should call handleTA when assign TA button is clicked", async () => { + const actionsColumn = courseColumns( + mockHandleEdit, + mockHandleDelete, + mockHandleTA, + mockHandleCopy, + //"Super Administrator" + ).find((col) => col.id === "actions"); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const assignTAButton = screen.getByTestId("assign-ta"); + + userEvent.click(assignTAButton); + expect(mockHandleTA).toHaveBeenCalledTimes(1); + expect(mockHandleTA).toHaveBeenCalledWith(mockRow); + }); + + test("should call handleCopy when copy button is clicked", async () => { + const actionsColumn = courseColumns( + mockHandleEdit, + mockHandleDelete, + mockHandleTA, + mockHandleCopy, + //"Super Administrator" + ).find((col) => col.id === "actions"); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const copyButton = screen.getByTestId("copy"); + + userEvent.click(copyButton); + expect(mockHandleCopy).toHaveBeenCalledTimes(1); + expect(mockHandleCopy).toHaveBeenCalledWith(mockRow); + }); +}); diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index fd06e79f..9e597501 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -1,7 +1,12 @@ import { createColumnHelper, Row } from "@tanstack/react-table"; import { Button, Tooltip, OverlayTrigger, Badge } from "react-bootstrap"; import { ICourseResponse as ICourse } from "../../utils/interfaces"; +import { formatDate } from "../../utils/util"; +/** + * @author Atharva Thorve, on December, 2023 + * @author Mrityunjay Joshi on December, 2023 + */ type Fn = (row: Row) => void; @@ -35,6 +40,7 @@ export const courseColumns = ( columnHelper.accessor("institution.name", { id: "institution", + size: 250, header: () => ( ( (
- - {new Date(info.getValue()).toLocaleDateString() || ( - N/A - )} + {formatDate(info.getValue() as unknown as string)}
), @@ -115,6 +119,7 @@ export const courseColumns = ( }), columnHelper.accessor("updated_at", { + size: 200, header: () => ( (
- - {new Date(info.getValue()).toLocaleDateString() || ( - N/A - )} + {formatDate(info.getValue() as unknown as string)}
), @@ -157,9 +159,9 @@ export const courseColumns = ( className="p-0" > Edit @@ -172,9 +174,9 @@ export const courseColumns = ( className="p-0" > Delete @@ -187,9 +189,9 @@ export const courseColumns = ( className="p-0" > Assign TA @@ -204,7 +206,7 @@ export const courseColumns = ( Copy diff --git a/src/pages/Courses/CourseEditor.tsx b/src/pages/Courses/CourseEditor.tsx index e02deec1..46e3fe6b 100644 --- a/src/pages/Courses/CourseEditor.tsx +++ b/src/pages/Courses/CourseEditor.tsx @@ -1,14 +1,14 @@ -import FormCheckBoxGroup from "../../components/Form/FormCheckBoxGroup"; -import FormInput from "../../components/Form/FormInput"; -import FormSelect from "../../components/Form/FormSelect"; + import FormCheckBoxGroup from "components/Form/FormCheckBoxGroup"; +import FormInput from "components/Form/FormInput"; +import FormSelect from "components/Form/FormSelect"; import { Form, Formik, FormikHelpers } from "formik"; -import useAPI from "../../hooks/useAPI"; +import useAPI from "hooks/useAPI"; import React, { useEffect, useState } from "react"; import { Button, InputGroup, Modal } from "react-bootstrap"; import { useDispatch, useSelector } from "react-redux"; import { useLoaderData, useLocation, useNavigate } from "react-router-dom"; -import { alertActions } from "../../store/slices/alertSlice"; // Success message utility -import { HttpMethod } from "../../utils/httpMethods"; +import { alertActions } from "store/slices/alertSlice"; // Success message utility +import { HttpMethod } from "utils/httpMethods"; import * as Yup from "yup"; import { RootState } from "../../store/store"; import { IEditor, ROLE } from "../../utils/interfaces"; diff --git a/src/pages/Courses/CourseUtil.ts b/src/pages/Courses/CourseUtil.ts index 3e776540..ea5239d4 100644 --- a/src/pages/Courses/CourseUtil.ts +++ b/src/pages/Courses/CourseUtil.ts @@ -97,10 +97,16 @@ export async function loadCourseInstructorDataAndInstitutions({ params }: any) { } // Load institutions data - const institutionsResponse = await axiosClient.get("/institutions", { - transformResponse: transformInstitutionsResponse, - }); - const institutions = await institutionsResponse.data; + let institutions = []; + try { + const institutionsResponse = await axiosClient.get("/institutions", { + transformResponse: transformInstitutionsResponse, + }); + institutions = await institutionsResponse.data; + } catch (error) { + console.error("Failed to load institutions:", error); + // Default to an empty array on error to prevent crashing + } // ToDo: Create an API to just fetch instructors, so here in the frontend we won't have to filter out the users based on the role. const usersResponse = await axiosClient.get("/users", { diff --git a/src/pages/TA/TA.tsx b/src/pages/TA/TA.tsx index c1eed5ac..d38ce406 100644 --- a/src/pages/TA/TA.tsx +++ b/src/pages/TA/TA.tsx @@ -12,6 +12,7 @@ import { alertActions } from "../../store/slices/alertSlice"; import { RootState } from "../../store/store"; import { ITAResponse, ROLE } from "../../utils/interfaces"; import { TAColumns as TA_COLUMNS } from "./TAColumns"; +import ColumnButton from "../../components/ColumnButton"; import DeleteTA from "./TADelete"; /** @@ -85,26 +86,41 @@ const TAs = () => {
-
- + + navigate("new")} + tooltip="Add TA to this course" + icon={Assign TA} + /> - {showDeleteConfirmation.visible && ( - - )} - - -
+ {tableData.length === 0 ? ( + + +

No TAs are assigned for this course.

+ + + ) : ( + +
+ + )} diff --git a/src/pages/TA/TAColumns.test.tsx b/src/pages/TA/TAColumns.test.tsx new file mode 100644 index 00000000..d1e136f2 --- /dev/null +++ b/src/pages/TA/TAColumns.test.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Row } from "@tanstack/react-table"; +import { TAColumns } from "./TAColumns"; + +// Mock the ColumnButton component +jest.mock("../../components/ColumnButton", () => ({ id, ...props }: any) => ( + +)); + +describe("TAColumns", () => { + const mockHandleDelete = jest.fn(); + const mockRow: Partial> = { original: { id: "123", name: "Test TA" } }; + + test("should define all required columns", () => { + const columns = TAColumns(mockHandleDelete); + expect(columns).toHaveLength(5); + + // Check each column's header + expect(columns[0].header).toBe("Id"); + expect(columns[1].header).toBe("TA Name"); + expect(columns[2].header).toBe("Full Name"); + expect(columns[3].header).toBe("Email"); + expect(columns[4].header).toBe("Actions"); + }); + + test("should correctly render the actions column", () => { + const actionsColumn = TAColumns(mockHandleDelete).find((col) => col.id === "actions"); + expect(actionsColumn).toBeDefined(); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const deleteButton = screen.getByTestId("delete-ta"); + expect(deleteButton).toBeTruthy(); + }); + + test("should call handleDelete when delete button is clicked", async () => { + const actionsColumn = TAColumns(mockHandleDelete).find((col) => col.id === "actions"); + const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; + + render(} />); + const deleteButton = screen.getByTestId("delete-ta"); + + await userEvent.click(deleteButton); + expect(mockHandleDelete).toHaveBeenCalledTimes(1); + expect(mockHandleDelete).toHaveBeenCalledWith(mockRow); + }); +}); diff --git a/src/pages/TA/TAColumns.tsx b/src/pages/TA/TAColumns.tsx index c4545b5f..fd7510cd 100644 --- a/src/pages/TA/TAColumns.tsx +++ b/src/pages/TA/TAColumns.tsx @@ -3,6 +3,7 @@ import { createColumnHelper, Row } from "@tanstack/react-table"; import { Button } from "react-bootstrap"; import { BsPersonXFill } from "react-icons/bs"; import { ITAResponse as ITA } from "../../utils/interfaces"; +import ColumnButton from "../../components/ColumnButton"; /** * @author Atharva Thorve, on December, 2023 @@ -38,14 +39,19 @@ export const TAColumns = (handleDelete: Fn) => [ header: "Actions", cell: ({ row }) => ( <> - + tooltip="Delete TA" + icon={Delete} + /> ), }), diff --git a/src/pages/TA/TAEditor.tsx b/src/pages/TA/TAEditor.tsx index 7df40ccd..dea46031 100644 --- a/src/pages/TA/TAEditor.tsx +++ b/src/pages/TA/TAEditor.tsx @@ -1,8 +1,7 @@ -// Importing necessary interfaces and modules -import FormSelect from "../../components/Form/FormSelect"; +import React, { useEffect, useState } from "react"; +import Select from 'react-select'; import { Form, Formik, FormikHelpers } from "formik"; -import useAPI from "../../hooks/useAPI"; -import React, { useEffect } from "react"; +import useAPI from "hooks/useAPI"; import { Button, InputGroup, Modal } from "react-bootstrap"; import { useDispatch } from "react-redux"; import { useLoaderData, useLocation, useNavigate, useParams } from "react-router-dom"; @@ -13,10 +12,19 @@ import { IEditor } from "../../utils/interfaces"; import { ITAFormValues, transformTARequest } from "./TAUtil"; /** - * @author Atharva Thorve, on December, 2023 - * @author Divit Kalathil, on December, 2023 + * @author Anurag Gorkar, on December, 2024 + * @author Makarand Pundalik, on December, 2024 + * @author Rutvik Kulkarni, on December, 2024 */ + +// Type definition for user options +type UserOption = { + label: string; + value: string | number; + role?: string; +}; + const initialValues: ITAFormValues = { name: "", }; @@ -29,7 +37,6 @@ const TAEditor: React.FC = ({ mode }) => { const { data: TAResponse, error: TAError, sendRequest } = useAPI(); const TAData = { ...initialValues }; - // Load data from the server const { taUsers }: any = useLoaderData(); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -37,9 +44,9 @@ const TAEditor: React.FC = ({ mode }) => { const params = useParams(); const { courseId } = params; - // logged-in TA is the parent of the TA being created and the institution is the same as the parent's + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [selectedUser, setSelectedUser] = useState({ label: "", value: "" }); - // Close the modal if the TA is updated successfully and navigate to the TAs page useEffect(() => { if (TAResponse && TAResponse.status >= 200 && TAResponse.status < 300) { dispatch( @@ -48,80 +55,130 @@ const TAEditor: React.FC = ({ mode }) => { message: `TA ${TAData.name} ${mode}d successfully!`, }) ); - navigate(location.state?.from ? location.state.from : "/TAs"); + navigate(location.state?.from ? location.state.from : `/courses/${courseId}/tas`); } - }, [dispatch, mode, navigate, TAData.name, TAResponse, location.state?.from]); + }, [dispatch, mode, navigate, TAData.name, TAResponse, location.state?.from, showConfirmModal]); - // Show the error message if the TA is not updated successfully useEffect(() => { TAError && dispatch(alertActions.showAlert({ variant: "danger", message: TAError })); }, [TAError, dispatch]); const onSubmit = (values: ITAFormValues, submitProps: FormikHelpers) => { + const selectedUserData = taUsers.find((user: UserOption) => + parseInt(String(user.value)) === parseInt(String(values.name)) + ); + + if (selectedUserData?.role === 'student') { + // If selected user is a student, show confirmation modal + console.log("Student role detected...", selectedUserData); + setSelectedUser(selectedUserData); + setShowConfirmModal(true); + } else { + // If TA or other role, directly submit + submitTA(values); + } + submitProps.setSubmitting(false); + }; + + const submitTA = (values: ITAFormValues) => { let method: HttpMethod = HttpMethod.GET; - // ToDo: Need to create API in the backend for this call. - // Note: The current API needs the TA id to create a new TA which is incorrect and needs to be fixed. - // Currently we send the username of the user we want to add as the TA for the course. let url: string = `/courses/${courseId}/add_ta/${values.name}`; - // to be used to display message when TA is created sendRequest({ url: url, method: method, data: {}, transformRequest: transformTARequest, }); - submitProps.setSubmitting(false); + }; + + const handleConfirmAddStudent = () => { + // Submit TA addition if confirmed + submitTA({ name: String(selectedUser.value) }); + setShowConfirmModal(false); }; const handleClose = () => navigate(location.state?.from ? location.state.from : `/courses/${courseId}/tas`); - //Validation of TA Entry + return ( - - - Add TA - - - {TAError &&

{TAError}

} - - {(formik) => { - return ( -
- TA - } - /> - - - - - - - ); - }} -
-
-
+ <> + + + Add TA + + + {TAError &&

{TAError}

} + + {(formik) => { + return ( +
+
+ +