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"
>
@@ -172,9 +174,9 @@ export const courseColumns = (
className="p-0"
>
@@ -187,9 +189,9 @@ export const courseColumns = (
className="p-0"
>
@@ -204,7 +206,7 @@ export const courseColumns = (
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={
}
+ />
- {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={
}
+ />
>
),
}),
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 (
-
- );
- }}
-
-
-
+ <>
+
+
+ Add TA
+
+
+ {TAError && {TAError}
}
+
+ {(formik) => {
+ return (
+
+ );
+ }}
+
+
+
+
+ {/* Confirmation Modal for Student */}
+ setShowConfirmModal(false)} centered>
+
+ Confirm Adding Student as TA
+
+
+ Are you sure you want to add {selectedUser.label} (a student) as a Teaching Assistant for this course?
+ This action will convert {selectedUser.label} to a TA.
+
+
+
+
+
+
+ >
);
};
-export default TAEditor;
+export default TAEditor;
\ No newline at end of file
diff --git a/src/pages/TA/TAUtil.ts b/src/pages/TA/TAUtil.ts
index 946cecb5..1f684339 100644
--- a/src/pages/TA/TAUtil.ts
+++ b/src/pages/TA/TAUtil.ts
@@ -4,8 +4,9 @@ import axiosClient from "../../utils/axios_client";
import { ITA, ITARequest } from "../../utils/interfaces";
/**
- * @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
*/
/**
@@ -17,7 +18,7 @@ export interface ITAFormValues {
}
export const transformTAResponse = (taList: string) => {
- let taData: IFormOption[] = [{ label: "Select a TA", value: "" }];
+ let taData: IFormOption[] = [];
let tas: ITA[] = JSON.parse(taList);
tas.forEach((ta) => taData.push({ label: ta.name, value: ta.id! }));
return taData;
@@ -37,7 +38,16 @@ export async function loadTAs({ params }: any) {
const taRoleUsersResponse = await axiosClient.get(`/users/role/Teaching Assistant`, {
transformResponse: transformTAResponse
});
- const taUsers = taRoleUsersResponse.data;
+ let taUsers = taRoleUsersResponse.data;
+
+ // Making a GET request to fetch users with the "Student" role
+ const studentRoleUsersResponse = await axiosClient.get(`/users/role/Student`, {
+ transformResponse: transformTAResponse
+ });
+ let studentUsers = studentRoleUsersResponse.data;
+ for(let i=0; i = ({ mode }) => {
const { data: userResponse, error: userError, sendRequest } = useAPI();
const auth = useSelector(
@@ -97,11 +114,11 @@ const UserEditor: React.FC = ({ mode }) => {
const handleClose = () => navigate(location.state?.from ? location.state.from : "/users");
return (
-
-
+
+
{mode === "update" ? "Update User" : "Create User"}
-
+
{userError && {userError}
}
= ({ mode }) => {
{(formik) => {
return (