From 147c947ea9c9b1b1a58d29c251d799f5a43bd3fd Mon Sep 17 00:00:00 2001 From: MakarandPundlik <65530539+MakarandPundlik@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:29:24 -0500 Subject: [PATCH 01/13] Updated README.md added contributors name --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index b87cb004..97749e4d 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,8 @@ You don’t have to ever use `eject`. The curated feature set is suitable for sm You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). + +## Contributers +Makarand Pundlik +Anurag Gorkar +Rutvik Kulkarni From 159ca63a61966d63030180e47aaa45c942d91398 Mon Sep 17 00:00:00 2001 From: MakarandPundlik <65530539+MakarandPundlik@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:37:33 -0500 Subject: [PATCH 02/13] Added npm install command --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 97749e4d..542d9421 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.\ From 52d4f29645a4bb1e9dcc6eb45df6f89d5273f048 Mon Sep 17 00:00:00 2001 From: Rutvik Vishwas Kulkarni Date: Sun, 24 Nov 2024 17:50:33 -0500 Subject: [PATCH 03/13] UI enhancements to the TA and Course page --- src/components/ColumnButton.tsx | 57 +++++++++++++++++++++++++++++ src/pages/Courses/CourseColumns.tsx | 43 +++++++++++++++------- src/pages/TA/TA.tsx | 48 +++++++++++++++--------- src/pages/TA/TAColumns.tsx | 10 +++-- 4 files changed, 122 insertions(+), 36 deletions(-) create mode 100644 src/components/ColumnButton.tsx 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 = ( + + {icon} + + ); + + if (tooltip) { + return ( + {tooltip}} + > + {displayButton} + + ); + } + + return displayButton; + }; + + export default ColumnButton; \ No newline at end of file diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 25002d6f..59665c3c 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -3,6 +3,8 @@ import { Button } from "react-bootstrap"; import { BsPencilFill, BsPersonXFill } from "react-icons/bs"; import { MdContentCopy, MdDelete } from "react-icons/md"; import { ICourseResponse as ICourse } from "../../utils/interfaces"; +import ColumnButton from "../../components/ColumnButton"; + /** * @author Atharva Thorve, on December, 2023 @@ -53,28 +55,41 @@ export const courseColumns = (handleEdit: Fn, handleDelete: Fn, handleTA: Fn, ha header: "Actions", cell: ({ row }) => ( <> - handleEdit(row)}> - - - handleEdit(row)} + tooltip="Edit this course" + icon={} + /> + handleDelete(row)} - > - - - handleTA(row)}> - - - } + /> + handleTA(row)} + tooltip="Assign a TA to this course" + icon={} + /> + handleCopy(row)} - > - - + tooltip="Copy course details" + icon={} + /> > ), }), diff --git a/src/pages/TA/TA.tsx b/src/pages/TA/TA.tsx index 87a1b334..f15ffcb5 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,37 @@ const TAs = () => { - - navigate("new")}> - - + + 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.tsx b/src/pages/TA/TAColumns.tsx index c4545b5f..03b7b88f 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,15 @@ export const TAColumns = (handleDelete: Fn) => [ header: "Actions", cell: ({ row }) => ( <> - handleDelete(row)} - > - - + tooltip="Delete TA" + icon={} + /> > ), }), From 65421d9775a8c44b8af54d12afd1afe60802bcd5 Mon Sep 17 00:00:00 2001 From: MakarandPundlik Date: Thu, 28 Nov 2024 17:47:51 -0500 Subject: [PATCH 04/13] resolved a bug by adding select TA to the final array and removed it from the interface --- src/pages/TA/TAUtil.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/TA/TAUtil.ts b/src/pages/TA/TAUtil.ts index d49d99fa..c7a34f91 100644 --- a/src/pages/TA/TAUtil.ts +++ b/src/pages/TA/TAUtil.ts @@ -17,7 +17,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 +37,7 @@ 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; + taUsers = [{label: "Select a TA", value: ""},...taUsers]; return { taUsers }; } From 86265917854455dfd92d585ce10f68bcbd807025 Mon Sep 17 00:00:00 2001 From: MakarandPundlik Date: Sat, 30 Nov 2024 12:10:52 -0500 Subject: [PATCH 05/13] added unit test cases for columns --- src/pages/TA/TAColumns.test.tsx | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/pages/TA/TAColumns.test.tsx diff --git a/src/pages/TA/TAColumns.test.tsx b/src/pages/TA/TAColumns.test.tsx new file mode 100644 index 00000000..eb411a12 --- /dev/null +++ b/src/pages/TA/TAColumns.test.tsx @@ -0,0 +1,51 @@ +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(); + // expect(deleteButton).toContain("Delete TA"); + // expect(deleteButton).toHaveAttribute("tooltip", "Delete TA"); + }); + + 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); + }); +}); From 104268ac0c5a1a20b63c8e345bd404f604bb4818 Mon Sep 17 00:00:00 2001 From: MakarandPundlik Date: Sat, 30 Nov 2024 12:14:31 -0500 Subject: [PATCH 06/13] added unit test cases for coursecolumns --- src/pages/Courses/CourseColumns.test.tsx | 101 +++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/pages/Courses/CourseColumns.test.tsx diff --git a/src/pages/Courses/CourseColumns.test.tsx b/src/pages/Courses/CourseColumns.test.tsx new file mode 100644 index 00000000..03436a51 --- /dev/null +++ b/src/pages/Courses/CourseColumns.test.tsx @@ -0,0 +1,101 @@ +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 { courseColumns } from "./CourseColumns"; +// import ColumnButton from "../../components/ColumnButton"; + +// Mock the ColumnButton component +jest.mock("../../components/ColumnButton", () => ({ 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 + ).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 + ).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 + ).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 + ).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); + }); +}); From d3da1da8cd83952613b8b3372bc379259d4e56b0 Mon Sep 17 00:00:00 2001 From: MakarandPundlik Date: Sat, 30 Nov 2024 12:19:30 -0500 Subject: [PATCH 07/13] added test cases for column button --- src/components/ColumnButton.test.tsx | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/components/ColumnButton.test.tsx 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) => {children}, + 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(); + }); +}); From bd54b4e223cee11357884ff1f8dee2961a19f167 Mon Sep 17 00:00:00 2001 From: Anurag Dilip Gorkar Date: Sat, 30 Nov 2024 15:04:08 -0500 Subject: [PATCH 08/13] Added select feature to Add TA drop down --- package-lock.json | 274 ++++++++++++++++++++++++++++- package.json | 5 +- src/App.test.tsx | 9 - src/hooks/useAPI.ts | 2 +- src/pages/Authentication/Login.tsx | 2 +- src/pages/TA/TAEditor.tsx | 178 ++++++++++++------- src/utils/axios_client.ts | 2 +- 7 files changed, 385 insertions(+), 87 deletions(-) delete mode 100644 src/App.test.tsx diff --git a/package-lock.json b/package-lock.json index 3ef4561e..aa257d7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,8 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^3.7.0", - "formik": "^2.2.9", + "chart.js": "^4.1.1", + "formik": "^2.4.6", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", "react": "^18.2.0", @@ -38,7 +38,8 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.12.3", + "react-select": "^5.8.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -2393,6 +2394,147 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz", + "integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz", + "integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2486,6 +2628,31 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", @@ -3045,6 +3212,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5760,9 +5933,16 @@ } }, "node_modules/chart.js": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", - "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", + "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -8363,6 +8543,12 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8563,15 +8749,16 @@ } }, "node_modules/formik": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.5.tgz", - "integrity": "sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", + "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", "funding": [ { "type": "individual", "url": "https://opencollective.com/formik" } ], + "license": "Apache-2.0", "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "deepmerge": "^2.1.1", @@ -11372,6 +11559,12 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -14168,6 +14361,27 @@ } } }, + "node_modules/react-select": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz", + "integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-smooth": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", @@ -15628,6 +15842,12 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -16529,6 +16749,20 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -16603,6 +16837,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/package.json b/package.json index a1ed9d64..668f6cb0 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,7 @@ "axios": "^1.4.0", "bootstrap": "^5.3.3", "chart.js": "^4.1.1", - "recharts": "^2.0.0", - "formik": "^2.2.9", + "formik": "^2.4.6", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", "react": "^18.2.0", @@ -34,6 +33,8 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", + "react-select": "^5.8.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index 2a68616d..00000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/hooks/useAPI.ts b/src/hooks/useAPI.ts index 47ba7ee5..6db9fa61 100644 --- a/src/hooks/useAPI.ts +++ b/src/hooks/useAPI.ts @@ -6,7 +6,7 @@ import { getAuthToken } from "../utils/auth"; * @author Ankur Mundra on April, 2023 */ -axios.defaults.baseURL = "http://localhost:3002/api/v1"; +axios.defaults.baseURL = "http://localhost:3000/api/v1"; axios.defaults.headers.common["Accept"] = "application/json"; axios.defaults.headers.post["Content-Type"] = "application/json"; axios.defaults.headers.put["Content-Type"] = "application/json"; diff --git a/src/pages/Authentication/Login.tsx b/src/pages/Authentication/Login.tsx index 2051b297..d7bf781d 100644 --- a/src/pages/Authentication/Login.tsx +++ b/src/pages/Authentication/Login.tsx @@ -30,7 +30,7 @@ const Login: React.FC = () => { const onSubmit = (values: ILoginFormValues, submitProps: FormikHelpers) => { axios - .post("http://localhost:3002/login", values) + .post("http://localhost:3000/login", values) .then((response) => { const payload = setAuthToken(response.data.token); diff --git a/src/pages/TA/TAEditor.tsx b/src/pages/TA/TAEditor.tsx index 6d742c05..86c97fa7 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 { Button, InputGroup, Modal } from "react-bootstrap"; import { useDispatch } from "react-redux"; import { useLoaderData, useLocation, useNavigate, useParams } from "react-router-dom"; @@ -12,10 +11,12 @@ import * as Yup from "yup"; import { IEditor } from "../../utils/interfaces"; import { ITAFormValues, transformTARequest } from "./TAUtil"; -/** - * @author Atharva Thorve, on December, 2023 - * @author Divit Kalathil, on December, 2023 - */ +// Type definition for user options +type UserOption = { + label: string; + value: string | number; + role?: string; +}; const initialValues: ITAFormValues = { name: "", @@ -29,7 +30,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 +37,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 +48,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 - } - /> - - - Close - - - - "Add TA" - - - - ); - }} - - - + <> + + + Add TA + + + {TAError && {TAError}} + + {(formik) => { + return ( + + + Teaching Assistant Name + option.value === formik.values.name) || null} + onChange={(selectedOption: UserOption | null) => { + formik.setFieldValue('name', selectedOption ? selectedOption.value : ''); + }} + placeholder="Search and select Teaching Assistant" + isSearchable={true} + // Custom filtering to search through label and value + filterOption={(option, inputValue) => { + const label = String(option.label).toLowerCase(); + const value = String(option.value).toLowerCase(); + const input = inputValue.toLowerCase(); + return label.includes(input) || value.includes(input); + }} + /> + + + + Close + + + + Add TA + + + + ); + }} + + + + + {/* 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. + + + setShowConfirmModal(false)}> + Cancel + + + Confirm + + + + > ); }; -export default TAEditor; +export default TAEditor; \ No newline at end of file diff --git a/src/utils/axios_client.ts b/src/utils/axios_client.ts index b5fe47d7..2297a8de 100644 --- a/src/utils/axios_client.ts +++ b/src/utils/axios_client.ts @@ -6,7 +6,7 @@ import { getAuthToken } from "./auth"; */ const axiosClient = axios.create({ - baseURL: "http://localhost:3002/api/v1", + baseURL: "http://localhost:3000/api/v1", timeout: 1000, headers: { "Content-Type": "application/json", From 57bc5e43480c26544800d43fe7a25f8449f1bd95 Mon Sep 17 00:00:00 2001 From: Rutvik Kulkarni <44207349+Rutvik2598@users.noreply.github.com> Date: Sat, 30 Nov 2024 20:36:36 -0500 Subject: [PATCH 09/13] Fetch students as well as TAs in the Add TA page - Add TA only requests Users with role TA. - This should also fetch Users with roles students. --- src/pages/TA/TAUtil.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/TA/TAUtil.ts b/src/pages/TA/TAUtil.ts index c7a34f91..a538f680 100644 --- a/src/pages/TA/TAUtil.ts +++ b/src/pages/TA/TAUtil.ts @@ -38,6 +38,15 @@ export async function loadTAs({ params }: any) { transformResponse: transformTAResponse }); let taUsers = taRoleUsersResponse.data; - taUsers = [{label: "Select a TA", value: ""},...taUsers]; + + // 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 Date: Mon, 2 Dec 2024 20:36:14 -0500 Subject: [PATCH 10/13] Added permission check before rendering add TA button --- src/App.test.tsx | 9 +++++++++ src/pages/Courses/Course.tsx | 3 ++- src/pages/Courses/CourseColumns.test.tsx | 14 +++++++++----- src/pages/Courses/CourseColumns.tsx | 12 +++++++----- src/pages/TA/TAEditor.tsx | 7 +++++++ src/pages/TA/TAUtil.ts | 5 +++-- 6 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 src/App.test.tsx diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 00000000..2a68616d --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index d1e4db04..53ef2ccd 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -27,6 +27,7 @@ const Courses = () => { (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(); @@ -96,7 +97,7 @@ const Courses = () => { ); const tableColumns = useMemo( - () => COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle), + () => COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle, currUserRole), [onDeleteHandle, onEditHandle, onTAHandle, onCopyHandle] ); diff --git a/src/pages/Courses/CourseColumns.test.tsx b/src/pages/Courses/CourseColumns.test.tsx index 03436a51..ceff89de 100644 --- a/src/pages/Courses/CourseColumns.test.tsx +++ b/src/pages/Courses/CourseColumns.test.tsx @@ -20,7 +20,7 @@ describe("courseColumns", () => { }; test("should define all required columns", () => { - const columns = courseColumns(mockHandleEdit, mockHandleDelete, mockHandleTA, mockHandleCopy); + const columns = courseColumns(mockHandleEdit, mockHandleDelete, mockHandleTA, mockHandleCopy, "Super Administrator"); expect(columns).toHaveLength(5); // Check each column's header @@ -36,7 +36,8 @@ describe("courseColumns", () => { mockHandleEdit, mockHandleDelete, mockHandleTA, - mockHandleCopy + mockHandleCopy, + "Super Administrator" ).find((col) => col.id === "actions"); const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; @@ -53,7 +54,8 @@ describe("courseColumns", () => { mockHandleEdit, mockHandleDelete, mockHandleTA, - mockHandleCopy + mockHandleCopy, + "Super Administrator" ).find((col) => col.id === "actions"); const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; @@ -70,7 +72,8 @@ describe("courseColumns", () => { mockHandleEdit, mockHandleDelete, mockHandleTA, - mockHandleCopy + mockHandleCopy, + "Super Administrator" ).find((col) => col.id === "actions"); const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; @@ -87,7 +90,8 @@ describe("courseColumns", () => { mockHandleEdit, mockHandleDelete, mockHandleTA, - mockHandleCopy + mockHandleCopy, + "Super Administrator" ).find((col) => col.id === "actions"); const CellComponent = actionsColumn?.cell as React.FC<{ row: Row }>; diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 59665c3c..36edeb93 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -2,19 +2,20 @@ import { createColumnHelper, Row } from "@tanstack/react-table"; import { Button } from "react-bootstrap"; import { BsPencilFill, BsPersonXFill } from "react-icons/bs"; import { MdContentCopy, MdDelete } from "react-icons/md"; -import { ICourseResponse as ICourse } from "../../utils/interfaces"; +import { ICourseResponse as ICourse, ROLE } from "../../utils/interfaces"; import ColumnButton from "../../components/ColumnButton"; /** - * @author Atharva Thorve, on December, 2023 - * @author Mrityunjay Joshi on December, 2023 + * @author Anurag Gorkar, on December, 2024 + * @author Makarand Pundalik, on December, 2024 + * @author Rutvik Kulkarni, on December, 2024 */ // Course Columns Configuration: Defines the columns for the courses table type Fn = (row: Row) => void; const columnHelper = createColumnHelper(); -export const courseColumns = (handleEdit: Fn, handleDelete: Fn, handleTA: Fn, handleCopy: Fn) => [ +export const courseColumns = (handleEdit: Fn, handleDelete: Fn, handleTA: Fn, handleCopy: Fn, currUserRole: String) => [ // Column for the course name columnHelper.accessor("name", { id: "name", @@ -72,6 +73,7 @@ export const courseColumns = (handleEdit: Fn, handleDelete: Fn, handleTA: Fn, ha tooltip="Delete this course" icon={} /> + {currUserRole === ROLE.SUPER_ADMIN.valueOf() && (// Only show this button if the user is an admin (role_id = 1) handleTA(row)} tooltip="Assign a TA to this course" icon={} - /> + />)} Date: Sat, 7 Dec 2024 11:09:55 -0500 Subject: [PATCH 11/13] Removed contributor names --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 542d9421..e95163fe 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,3 @@ You don’t have to ever use `eject`. The curated feature set is suitable for sm You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). - -## Contributers -Makarand Pundlik -Anurag Gorkar -Rutvik Kulkarni From 59e8b6e0cfe5b109d102c54e01b66b88c3614ed2 Mon Sep 17 00:00:00 2001 From: Rutvik Vishwas Kulkarni Date: Sat, 7 Dec 2024 12:31:58 -0500 Subject: [PATCH 12/13] Code cleanup --- src/hooks/useAPI.ts | 2 +- src/pages/Authentication/Login.tsx | 2 +- src/pages/Courses/CourseColumns.test.tsx | 1 - src/pages/TA/TAColumns.test.tsx | 2 -- src/utils/axios_client.ts | 2 +- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/hooks/useAPI.ts b/src/hooks/useAPI.ts index 6db9fa61..47ba7ee5 100644 --- a/src/hooks/useAPI.ts +++ b/src/hooks/useAPI.ts @@ -6,7 +6,7 @@ import { getAuthToken } from "../utils/auth"; * @author Ankur Mundra on April, 2023 */ -axios.defaults.baseURL = "http://localhost:3000/api/v1"; +axios.defaults.baseURL = "http://localhost:3002/api/v1"; axios.defaults.headers.common["Accept"] = "application/json"; axios.defaults.headers.post["Content-Type"] = "application/json"; axios.defaults.headers.put["Content-Type"] = "application/json"; diff --git a/src/pages/Authentication/Login.tsx b/src/pages/Authentication/Login.tsx index d7bf781d..2051b297 100644 --- a/src/pages/Authentication/Login.tsx +++ b/src/pages/Authentication/Login.tsx @@ -30,7 +30,7 @@ const Login: React.FC = () => { const onSubmit = (values: ILoginFormValues, submitProps: FormikHelpers) => { axios - .post("http://localhost:3000/login", values) + .post("http://localhost:3002/login", values) .then((response) => { const payload = setAuthToken(response.data.token); diff --git a/src/pages/Courses/CourseColumns.test.tsx b/src/pages/Courses/CourseColumns.test.tsx index ceff89de..fbe52543 100644 --- a/src/pages/Courses/CourseColumns.test.tsx +++ b/src/pages/Courses/CourseColumns.test.tsx @@ -3,7 +3,6 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Row } from "@tanstack/react-table"; import { courseColumns } from "./CourseColumns"; -// import ColumnButton from "../../components/ColumnButton"; // Mock the ColumnButton component jest.mock("../../components/ColumnButton", () => ({ id, ...props }: any) => ( diff --git a/src/pages/TA/TAColumns.test.tsx b/src/pages/TA/TAColumns.test.tsx index eb411a12..d1e136f2 100644 --- a/src/pages/TA/TAColumns.test.tsx +++ b/src/pages/TA/TAColumns.test.tsx @@ -33,8 +33,6 @@ describe("TAColumns", () => { render(} />); const deleteButton = screen.getByTestId("delete-ta"); expect(deleteButton).toBeTruthy(); - // expect(deleteButton).toContain("Delete TA"); - // expect(deleteButton).toHaveAttribute("tooltip", "Delete TA"); }); test("should call handleDelete when delete button is clicked", async () => { diff --git a/src/utils/axios_client.ts b/src/utils/axios_client.ts index 2297a8de..b5fe47d7 100644 --- a/src/utils/axios_client.ts +++ b/src/utils/axios_client.ts @@ -6,7 +6,7 @@ import { getAuthToken } from "./auth"; */ const axiosClient = axios.create({ - baseURL: "http://localhost:3000/api/v1", + baseURL: "http://localhost:3002/api/v1", timeout: 1000, headers: { "Content-Type": "application/json", From e0cf5dae6f4fd45ea08848e28997af647a458b32 Mon Sep 17 00:00:00 2001 From: Rutvik2598 Date: Sat, 7 Dec 2024 12:47:52 -0500 Subject: [PATCH 13/13] Instructor can also add TA other than Admin --- src/pages/Courses/CourseColumns.tsx | 177 ++++++++++++++++------------ 1 file changed, 99 insertions(+), 78 deletions(-) diff --git a/src/pages/Courses/CourseColumns.tsx b/src/pages/Courses/CourseColumns.tsx index 36edeb93..87ff85b9 100644 --- a/src/pages/Courses/CourseColumns.tsx +++ b/src/pages/Courses/CourseColumns.tsx @@ -5,94 +5,115 @@ import { MdContentCopy, MdDelete } from "react-icons/md"; import { ICourseResponse as ICourse, ROLE } from "../../utils/interfaces"; import ColumnButton from "../../components/ColumnButton"; - /** * @author Anurag Gorkar, on December, 2024 * @author Makarand Pundalik, on December, 2024 * @author Rutvik Kulkarni, on December, 2024 */ +/** + * Determines if the current user has the authority to add a TA to a course. + * Only users with roles SUPER_ADMIN, ADMIN, or INSTRUCTOR can assign TAs. + * @param {String} currUserRole - The role of the current user + * @returns {boolean} - True if the user has TA assignment authority, otherwise false + */ +const hasAddTAAuthority = (currUserRole: String): boolean => { + return ( + currUserRole === ROLE.SUPER_ADMIN.valueOf() || + currUserRole === ROLE.ADMIN.valueOf() || + currUserRole === ROLE.INSTRUCTOR.valueOf() + ); +}; + // Course Columns Configuration: Defines the columns for the courses table type Fn = (row: Row) => void; const columnHelper = createColumnHelper(); -export const courseColumns = (handleEdit: Fn, handleDelete: Fn, handleTA: Fn, handleCopy: Fn, currUserRole: String) => [ - // Column for the course name - columnHelper.accessor("name", { - id: "name", - header: "Name", - enableSorting: true, - enableColumnFilter: true, - enableGlobalFilter: false, - }), - // Column for the institution name - columnHelper.accessor("institution.name", { - id: "institution", - header: "Institution", - enableSorting: true, - enableMultiSort: true, - enableGlobalFilter: false, - }), +export const courseColumns = ( + handleEdit: Fn, + handleDelete: Fn, + handleTA: Fn, + handleCopy: Fn, + currUserRole: String +) => [ + // Column for the course name + columnHelper.accessor("name", { + id: "name", + header: "Name", + enableSorting: true, + enableColumnFilter: true, + enableGlobalFilter: false, + }), + + // Column for the institution name + columnHelper.accessor("institution.name", { + id: "institution", + header: "Institution", + enableSorting: true, + enableMultiSort: true, + enableGlobalFilter: false, + }), - // Column for the creation date - columnHelper.accessor("created_at", { - header: "Creation Date", - enableSorting: true, - enableColumnFilter: false, - enableGlobalFilter: false, - }), + // Column for the creation date + columnHelper.accessor("created_at", { + header: "Creation Date", + enableSorting: true, + enableColumnFilter: false, + enableGlobalFilter: false, + }), - // Column for the last updated date - columnHelper.accessor("updated_at", { - header: "Updated Date", - enableSorting: true, - enableColumnFilter: false, - enableGlobalFilter: false, - }), + // Column for the last updated date + columnHelper.accessor("updated_at", { + header: "Updated Date", + enableSorting: true, + enableColumnFilter: false, + enableGlobalFilter: false, + }), - // Actions column with edit, delete, TA, and copy buttons - columnHelper.display({ - id: "actions", - header: "Actions", - cell: ({ row }) => ( - <> - handleEdit(row)} - tooltip="Edit this course" - icon={} - /> - handleDelete(row)} - tooltip="Delete this course" - icon={} - /> - {currUserRole === ROLE.SUPER_ADMIN.valueOf() && (// Only show this button if the user is an admin (role_id = 1) - handleTA(row)} - tooltip="Assign a TA to this course" - icon={} - />)} - handleCopy(row)} - tooltip="Copy course details" - icon={} - /> - > - ), - }), -]; + // Actions column with edit, delete, TA, and copy buttons + columnHelper.display({ + id: "actions", + header: "Actions", + cell: ({ row }) => ( + <> + handleEdit(row)} + tooltip="Edit this course" + icon={} + /> + handleDelete(row)} + tooltip="Delete this course" + icon={} + /> + {hasAddTAAuthority(currUserRole) && ( // Use the helper function to determine TA authority + handleTA(row)} + tooltip="Assign a TA to this course" + icon={} + /> + )} + handleCopy(row)} + tooltip="Copy course details" + icon={} + /> + > + ), + }), + ]; \ No newline at end of file
{TAError}